![]() |
Web Development »
ASP.NET »
Utilities
Intermediate
License: The Code Project Open License (CPOL)
UFrame: goodness of UpdatePanel and IFRAME combinedBy Omar Al ZabirUFrame makes a DIV behave like an IFRAME that can load any ASP.NET/PHP/HTML page and allows all postback and hyperlink navigation to happen within the DIV - a painless way to make regular pages fully AJAX enabled |
Javascript, CSS, HTML, XHTML, ASP, ASP.NET, WebForms, Ajax, Architect, Design
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
Download source from www.codeplex.com/uframe
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 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 an 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.
You can test UFrame from:
UFrame an ultimate replacement for UpdatePanel 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:
UpdatePanel, or simple hyperlinks will behave as if content is being loaded using AJAX.
src property of DIVs and they are converted to UFrames when UFrame library loads.
UFrames.
UFrame postback.
You can download 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.
You just need to include three javascripts on your page:
All these comes 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.
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>
src attribute points to a MVC handled url. The result is as usual:The MVC View is implemented as following:
<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>
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: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. UFrames. 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 an 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)
{
//if(config.progressTemplate) container.html(config.progressTemplate);
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) // IE { styleNode.styleSheet.cssText = text; } else // w3c { 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. IE 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); });
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.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 is 0 when all external scripts get loaded either successfully or some fails 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 you information, adding a <script> tag to the <head> tag is not all you need to do. For some browser 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. // 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 ); } });
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)
{
// 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);
} );
}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 )
{
// 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;
}
},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 )
{
// 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);
});
}
}Here are the steps:
action URL
action URL
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 UFrames, 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.
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.
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.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 20 Sep 2009 Editor: |
Copyright 2008 by Omar Al Zabir Everything else Copyright © CodeProject, 1999-2010 Web17 | Advertise on the Code Project |