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

UFrame: Goodness of UpdatePanel and IFRAME Combined

0.00/5 (No votes)
11 Jun 2011 4  
UFrame 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

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 UFrames 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 UFrames 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.

image

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 DIVs 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:

image

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:

image

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 DIVs that want to be 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 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)
    {
        //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. 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:

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

}

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 )
    {
        // 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:

  • 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.

image

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.

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.

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