Update Panel .NET

Exploring Microsoft ASP.NET AJAX and jQuery

Archive for October 2008

Latest ASP.NET preview available to download

leave a comment »

The latest release of ASP.NET preview is available on CodePlex!

This release include:

  • ASP.NET AJAX 4.0 Preview 3
  • ASP.NET Dynamic Data 4.0 Preview 1
  • ASP.NET MVC Beta Source Code Release

This release showcases features that may be included in ASP.NET 4.0!

Written by tzkuei

October 28, 2008 at 4:39 pm

Posted in ASP.NET

Tagged with , , ,

Dual List Box Extender

with 3 comments

At work, one of the projects in development requires a dual list box control, so I thought I’d have a go at creating one.

Since the dual list box control is bascially an “enhanced” view of a multiple selectable list box, I have decided to create an extender for the standard ASP.NET ListBox control.

First, create your page, add the ListBox control and implement the required functionality.

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "<a href="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd</a>">
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>">
<head id="Head1" runat="server">
    <title>DualListBoxExtender</title>
</head>
<body>
    <form id="sample" runat="server">
<div>
        <asp:ListBox runat="server" ID="lstRoles" SelectionMode="Multiple" Rows="10" CssClass="updn" />
        <asp:Button runat="server" ID="btnSubmit" Text="Update" /></div>
    </form>
</body>
</html>

Create the server-side extender control:

using System;
using System.Collections.Generic;
using System.Web.UI;
using System.Web.UI.WebControls;
[assembly: WebResource("UPDN.DualListBoxExtender.js", "text/javascript")]
namespace UPDN
{
    ///

    /// Summary description for DualListBoxExtender
    ///

    [TargetControlType(typeof(ListBox))]
    public class DualListBoxExtender : ExtenderControl
    {
        protected override IEnumerable
                GetScriptDescriptors(System.Web.UI.Control targetControl)
        {
            ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor(“UPDN.DualListBoxExtender”, targetControl.ClientID);
            yield return descriptor;
        }
        protected override IEnumerable
                GetScriptReferences()
        {
            yield return new ScriptReference(“UPDN.DualListBoxExtender.js”, this.GetType().Assembly.FullName);
        }
    }
}

Create the client-side behavior script:

Type.registerNamespace("UPDN");
// Structure of generated controls:
// <div class="updn-duallist">
//     <select class="list1"></select>
//     <a class="add"></a>
//     <a class="remove"></a>
//     <select class="list2"></select>
// </div>
UPDN.DualListBoxExtender = function(element) {
    UPDN.DualListBoxExtender.initializeBase(this, [element]);
    this._list1 = [];   // list of indices of unselected items
    this._list2 = [];   // list of indices of selected items
    this._listBox1 = null;
    this._listBox2 = null;
    this._addButton = null;
    this._removeButton = null;
}
UPDN.DualListBoxExtender.prototype = {
    initialize: function() {
        UPDN.DualListBoxExtender.callBaseMethod(this, "initialize");
        var element = this.get_element();
        // Hide CheckBoxList control
        element.style.display = "none";
        // Get list items
        for (var i = 0; i < element.options.length; i++) {
            var option = element.options[i];
            if (option.selected) {
                this._list2.push(i);
            } else {
                this._list1.push(i);
            }
        }
        // Create container for dual list controls
        var container = document.createElement("div");
        container.className = element.className;
        if (element.nextSibling) {
            element.parentNode.insertBefore(container, element.nextSibling);
        } else {
            element.parentNode.appendChild(container);
        }
        // Create list box for unselected items
        this._listBox1 = document.createElement("select");
        this._listBox1.className = "list1";
        this._listBox1.setAttribute("multiple", "multiple");
        this._listBox1.setAttribute("size", element.size);
        container.appendChild(this._listBox1);
        // Create add link
        this._addButton = document.createElement("a");
        this._addButton.className = "add";
        this._addButton.href = "#";
        this._addButton.appendChild(document.createTextNode("Add >>"));
        container.appendChild(this._addButton);
        $addHandlers(this._addButton, { "click": this._addClick }, this);
        // Create remove link
        this._removeButton = document.createElement("a");
        this._removeButton.className = "remove";
        this._removeButton.href = "#";
        this._removeButton.appendChild(document.createTextNode("<< Remove"));
        container.appendChild(this._removeButton);
        $addHandlers(this._removeButton, { "click": this._removeClick }, this);
        // Create list box for selected items
        this._listBox2 = document.createElement("select");
        this._listBox2.className = "list2";
        this._listBox2.setAttribute("multiple", "multiple");
        this._listBox2.setAttribute("size", element.size);
        container.appendChild(this._listBox2);
        // Initialise lists
        this._bindLists();
    },
    dispose: function() {
        $clearHandlers(this._addButton);
        $clearHandlers(this._removeButton);
        UPDN.DualListBoxExtender.callBaseMethod(this, "dispose");
    },
    _bindList: function(list, listBox) {
        var element = this.get_element();
        // Sort list (based on order of CheckBoxList items)
        list.sort();
        // Clear list box options
        listBox.options.length = 0;
        for (var i = 0; i < list.length; i++) {
            var option = document.createElement("option");
            // Get the item index of check box elements list
            var item = list[i];
            // Get label from list of label elements
            option.text = element.options[item].text;
            option.value = item;
            // Add option to list box
            listBox.options[listBox.options.length] = option;
        }
    },
    _bindLists: function() {
        this._bindList(this._list1, this._listBox1);
        this._bindList(this._list2, this._listBox2);
    },
    _moveItem: function(listBox, fromList, toList, selected) {
        var element = this.get_element();
        for (var i = 0; i < listBox.options.length; i++) {
            var option = listBox.options[i];
            if (option.selected) {
                var item = Number(option.value);
                Array.add(toList, item);
                Array.remove(fromList, item);
                // Sets checked state of CheckBoxList item (for post-back)
                element.options[item].selected = selected;
            }
        }
        this._bindLists();
    },
    _addClick: function(ev) {
        ev.preventDefault();
        this._moveItem(this._listBox1, this._list1, this._list2, true);
    },
    _removeClick: function(ev) {
        ev.preventDefault();
        this._moveItem(this._listBox2, this._list2, this._list1, false);
    }
}
UPDN.DualListBoxExtender.registerClass("UPDN.DualListBoxExtender", Sys.UI.Behavior);

Add a ScriptManager control to your page, and the DualListBoxExtender, targeting the ListBox control:

<asp:ScriptManager ID="scriptManager" runat="server"></asp:ScriptManager>
<asp:ListBox runat="server" ID="lstRoles" SelectionMode="Multiple" Rows="10" CssClass="updn" />
<updn:DualListBoxExtender runat="server" TargetControlID="lstRoles" />

Style the dual list control:

.updn
{
    width: 400px;
    padding: 5px;
    /* auto clearing of floats */
    overflow: auto;
    /* positioning parent for add/remove links */
    position: relative;
}
.updn select
{
    font-family: Arial;
    font-size: small;
}
.updn a
{
    font-family: Arial;
    font-size: small;
    width: 85px;
    text-align: center;
    text-decoration: none;
}
.updn a:link, .updn a:visited
{
    border: solid 1px #ccc;
    background-color: #fff;
    color: #00f;
}
.updn a:hover, .updn a:active, .updn a:focus
{
    border: solid 1px #ccc;
    background-color: #fff;
    color: #f00;
}
.updn .list1
{
    width: 150px;
    float: left;
}
.updn .list2
{
    width: 150px;
    float: right;
}
.updn .add
{
    position: absolute;
    left: 160px;
    top: 50px;
}
.updn .remove
{
    position: absolute;
    left: 160px;
    top: 90px;
}

Download the complete solution from my SkyDrive.

Written by tzkuei

October 26, 2008 at 10:49 pm

UPDN.Binding – Refectored using the module pattern

leave a comment »

As my UPDN.Binding “class” contains two public static methods (add, remove), and four internal implementation methods (_indexOfBinding, _boundPropertyChanged, _add, _remove), I thought I’d refactor it to use the module pattern:

Type.registerNamespace("UPDN");

UPDN.Binding = function() {
    // Implementation
    function _indexOfBinding(targetBindings, targetComponent, targetPropertyName) {
        for (var i = 0, l = targetBindings.length; i < l; i++) {
            var targetBinding = targetBindings[i];
            if (targetBinding.component === targetComponent && targetBinding.propertyName === targetPropertyName) {
                return i;
            }
        }
        return -1;
    }
    function _boundPropertyChanged(sourceComponent, args) {
        var sourcePropertyName = args.get_propertyName();
        var targetBindings = sourceComponent.$bindings$[sourcePropertyName];
        if (targetBindings) {
            // Invoke property getter of source component
            var sourcePropertyValue = sourceComponent["get_" + sourcePropertyName]();
            for (var i = 0, l = targetBindings.length; i < l; i++) {
                var targetBinding = targetBindings[i];
                // Invoke property setter of target component
                targetBinding.component["set_" + targetBinding.propertyName](sourcePropertyValue);
            }
        }
    }
    function _add(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
        // Adds propertyChanged handler to sourceComponent
        sourceComponent.add_propertyChanged(_boundPropertyChanged);
        // Initialize source component bindings
        // Using the name $bindings$ to avoid clash
        if (!sourceComponent.$bindings$) {
            sourceComponent.$bindings$ = {};
        }
        // Initialize array to hold target bindings
        if (!(sourcePropertyName in sourceComponent.$bindings$)) {
            sourceComponent.$bindings$[sourcePropertyName] = [];
        }
        if (_indexOfBinding(sourceComponent.$bindings$[sourcePropertyName], targetComponent, targetPropertyName) === -1) {
            Array.add(sourceComponent.$bindings$[sourcePropertyName], { "component": targetComponent, "propertyName": targetPropertyName });
        }
    }
    function _remove(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
        // Removes propertyChanged handler from sourceComponent
        sourceComponent.remove_propertyChanged(_boundPropertyChanged);
        if (sourceComponent.$bindings$) {
            var targetBindings = sourceComponent.$bindings$[sourcePropertyName];
            if (targetBindings) {
                var i = _indexOfBinding(targetBindings, targetComponent, targetPropertyName);
                if (i !== -1) {
                    Array.removeAt(targetBindings, i);
                }
            }
        }
    }
    return {
        // Public methods
        add: function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
            _add(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName);
            _add(targetComponent, targetPropertyName, sourceComponent, sourcePropertyName);
        },
        remove: function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
            _remove(sourceComponent, sourcePropertyName);
            _remove(targetComponent, targetPropertyName);
        }
    };
} ();

Written by tzkuei

October 21, 2008 at 12:46 pm

Binding properties using propertyChanged event

leave a comment »

The Sys.Component class provides a propertyChanged event, and we can use that event to achieve property bindings.

The basic idea is, in the property setter, raise the property changed event when the value changes:

set_pageNumber: function(value) {
    if (value != this._pageNumber) {
        this._pageNumber = value;
        this.raisePropertyChanged("pageNumber");
    }
},
get_pageNumber: function() {
    return this._pageNumber;
}

This provides the hook for the Binding class, which registers a property changed event handler to propagate the change to all bound targets.

Type.registerNamespace("UPDN");

UPDN.Binding = function() {
    throw Error.invalidOperation();
}
UPDN.Binding.registerClass("UPDN.Binding");
// Implementation
UPDN.Binding._indexOfBinding = function(targetBindings, targetComponent, targetPropertyName) {
    for (var i = 0, l = targetBindings.length; i < l; i++) {
        var targetBinding = targetBindings[i];
        if (targetBinding.component === targetComponent && targetBinding.propertyName === targetPropertyName) {
            return i;
        }
    }
    return -1;
}
UPDN.Binding._boundPropertyChanged = function(sourceComponent, args) {
    var sourcePropertyName = args.get_propertyName();
    var targetBindings = sourceComponent.$bindings$[sourcePropertyName];
    if (targetBindings) {
        // Invoke property getter of source component
        var sourcePropertyValue = sourceComponent["get_" + sourcePropertyName]();
        for (var i = 0, l = targetBindings.length; i < l; i++) {
            var targetBinding = targetBindings[i];
            // Invoke property setter of target component
            targetBinding.component["set_" + targetBinding.propertyName](sourcePropertyValue);
        }
    }
}
UPDN.Binding._add = function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
    // Adds propertyChanged handler to sourceComponent
    sourceComponent.add_propertyChanged(UPDN.Binding._boundPropertyChanged);
    // Initialize source component bindings
    // Using the name $bindings$ to avoid clash
    if (!sourceComponent.$bindings$) {
        sourceComponent.$bindings$ = {};
    }
    // Initialize array to hold target bindings
    if (!(sourcePropertyName in sourceComponent.$bindings$)) {
        sourceComponent.$bindings$[sourcePropertyName] = [];
    }
    if (UPDN.Binding._indexOfBinding(sourceComponent.$bindings$[sourcePropertyName], targetComponent, targetPropertyName) === -1) {
        Array.add(sourceComponent.$bindings$[sourcePropertyName], { "component": targetComponent, "propertyName": targetPropertyName });
    }
}
UPDN.Binding._remove = function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
    // Removes propertyChanged handler from sourceComponent
    sourceComponent.remove_propertyChanged(UPDN.Binding._boundPropertyChanged);
    if (sourceComponent.$bindings$) {
        var targetBindings = sourceComponent.$bindings$[sourcePropertyName];
        if (targetBindings) {
            var i = UPDN.Binding._indexOfBinding(targetBindings, targetComponent, targetPropertyName);
            if (i !== -1) {
                Array.removeAt(targetBindings, i);
            }
        }
    }
}
// Public methods
UPDN.Binding.add = function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
    UPDN.Binding._add(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName);
    UPDN.Binding._add(targetComponent, targetPropertyName, sourceComponent, sourcePropertyName);
}
UPDN.Binding.remove = function(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName) {
    UPDN.Binding._remove(sourceComponent, sourcePropertyName);
    UPDN.Binding._remove(targetComponent, targetPropertyName);
}

Usage:

UPDN.Binding.add(pagerControl, "pageNumber", gridControl, "pageNumber");
UPDN.Binding.add(sliderControl, "volume", textBoxControl, "loudness");

UPDN.Binding.add(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName)

Binds sourcePropertyName of sourceComponent to targetPropertyName of targetComponent.

UPDN.Binding.remove(sourceComponent, sourcePropertyName, targetComponent, targetPropertyName)

Unbinds sourcePropertyName of sourceComponent to targetPropertyName of targetComponent.

With this implementation, all bindings are bi-directional, meaning that changes to a property of control A will be reflected in control B and vice versa.

Download Binding.js from my SkyDrive.

Written by tzkuei

October 18, 2008 at 6:50 pm

Making methods chainable in JavaScript

with one comment

With JavaScript, if you do not explicitly return “something” at the end of your method function, JavaScript returns the “undefined” value by default.

Therefore, the secret to making methods chainable is to explicitly return “something” at the end of each method.

I will illustrate this with an example:

DomElement Object

function DomElement(elementId) {
    this.element = document.getElementById(elementId);
}
DomElement.prototype = {
    setStyle: function(prop, val) {
        this.element.style[prop] = val;
    },
    show: function() {
        this.setStyle("display", "block");
    },
    hide: function() {
        this.setStyle("display", "none");
    }
}

To use the DomElement object, first create an instance with the new operator, passing in the ID of your DOM element, then invoke the methods on that instance:

var myPanel = new DomElement("myPanel");
myPanel.setStyle("border", "solid 2px #f00");
myPanel.setStyle("background", "#ffc");
myPanel.show();

You can rewrite the above using the with statement like this:

var myPanel = new DomElement("myPanel");
with (myPanel) {
    setStyle("border", "solid 2px #f00");
    setStyle("background", "#ffc");
    show();
}

Using the with statement makes the code slightly more expressive, but if we make all the methods chainable by explicitly returning the current instance:

function DomElement(elementId) {
    this.element = document.getElementById(elementId);
}
DomElement.prototype = {
    setStyle: function(prop, val) {
        this.element.style[prop] = val;
        return this;
    },
    show: function() {
        return this.setStyle("display", "block");
    },
    hide: function() {
        return this.setStyle("display", "none");
    }
}
// Add a shortcut method for creating a DomElement object:
$ = function(elementId) {
    return new DomElement(elementId);
}

We can now write code like this:

$("myPanel").setStyle("border", "solid 2px #f00").setStyle("background", "#ffc").show();

Chainable methods allow you to write really expressive code, and jQuery takes this idea even further.

Written by tzkuei

October 18, 2008 at 1:46 pm

Posted in DOM, JavaScript

Parsing Query String in JavaScript

leave a comment »

I have created a simple “class” to parse the query string, which I hope you may find it useful.

Type.registerNamespace("UPDN");

UPDN.QueryString = function(search) {
    this._queryString = {};
    if (search) {
        this.parse(search);
    }
}
UPDN.QueryString.prototype = {
    parse: function(search) {
        if (search.startsWith('?')) {
            search = search.substring(1);
        }
        var pairs = search.split('&');
        for (var i = 0; i < pairs.length; i++) {
            var pair = pairs[i].split('=');
            this.add(pair[0], pair[1]);
        }
    },
    getValue: function(name) {
        if (this._queryString[name]) {
            return this._queryString[name].toString();
        }
        return "";
    },
    getValues: function(name) {
        if (this._queryString[name]) {
            return this._queryString[name];
        } else {
            return [];
        }
    },
    add: function(name, value) {
        if (name) {
            if (!(name in this._queryString)) {
                this._queryString[name] = [];
            }
            if (value) {
                this._queryString[name].push(decodeURIComponent(value));
            }
        }
    },
    remove: function(name) {
        if (name in this._queryString) {
            delete _queryString[name];
        }
    },
    toString: function() {
        var pairs = [];
        for (var name in this._queryString) {
            var values = this._queryString[name];
            if (values.length > 0) {
                for (var i = 0; i < values.length; i++) {
                    pairs.push(name + '=' + encodeURIComponent(values[i].toString()));
                }
            } else {
                pairs.push(name);
            }
        }
        return pairs.join('&');
    }
}
UPDN.QueryString.registerClass("UPDN.QueryString");

Usage:

// Creates a new empty instance.
var queryString= new UPDN.QueryString();
// Creates a new instance with the search string of the current location.
var queryString= new UPDN.QueryString(location.search);

queryString.getValue(name)

Returns the string representation of the value associated with the given name.
If the given name has no match in the query string, an empty string will be returned.

queryString.getValues(name)

Returns an array of the values associated with the given name.
If the given name has no match in the query string, an empty array will be returned.

queryString.add(name, value)

Adds a value to the named query string parameter.

queryString.remove(name)

Removes all values associated to the given name.

queryString.toString()

Returns the string representation of the instance.

Download QueryString.js from my SkyDrive.

Written by tzkuei

October 18, 2008 at 10:43 am

Using $addHandlers() to reduce code bloat

leave a comment »

A tip for reducing code bloat is to use the $addHandlers method when adding multiple DOM event handlers to a single DOM element.

For example, instead of multiple calls to $addHandler:

$addHandler(element, "focus", Function.createDelegate(this, this._onTextBoxFocus));
$addHandler(element, "blur", Function.createDelegate(this, this._onTextBoxBlur));
$addHandler(element, "keydown", Function.createDelegate(this, this._onTextBoxKeyDown));
$addHandler(element, "keyup", Function.createDelegate(this, this._onTextBoxKeyUp));
$addHandler(element, "mousedown", Function.createDelegate(this, this._onTextBoxMouseDown));

You can save a lot of typing by using $addHandlers:

$addHandlers(element, {
   "focus": this._onTextBoxFocus,
   "blur": this._onTextBoxBlur,
   "keydown": this._onTextBoxKeyDown,
   "keyup": this._onTextBoxKeyUp,
   "mousedown": this._onTextBoxMouseDown
}, this);

Because “this” is passed in as the handler owner (third argument), a delegate will be created for each handler.

Written by tzkuei

October 18, 2008 at 8:50 am

Sys.Application initialization steps

leave a comment »

I think the most important lesson in building client-side components using the Microsoft AJAX Library, is to learn about the client-side lifecycle and the events that gets raised at each stage, so I looked into MicrosoftAjax.debug.js and noted down my observations:

When MicrosoftAjax.js is loaded, the Sys.Application (singleton) object is created:

Sys.Application = new Sys._Application();

Inside the constructor function, window load and unload events are hooked up:

Sys._Application = function Sys$_Application() {
    Sys._Application.initializeBase(this);
    ...
    this._unloadHandlerDelegate = Function.createDelegate(this, this._unloadHandler);
    this._loadHandlerDelegate = Function.createDelegate(this, this._loadHandler);
    Sys.UI.DomEvent.addHandler(window, "unload", this._unloadHandlerDelegate);
    Sys.UI.DomEvent.addHandler(window, "load", this._loadHandlerDelegate);
}

The _loadHandler method first unhooks the window load event handler, then invokes the initialize method:

function Sys$_Application$_loadHandler() {
    if (this._loadHandlerDelegate) {
        Sys.UI.DomEvent.removeHandler(window, "load", this._loadHandlerDelegate);
        this._loadHandlerDelegate = null;
    }
    this.initialize();
}

The initialize method defers invoking _doInitialize until the browser yields, using the setTimeout trick:

function Sys$_Application$initialize() {
    if(!this._initialized && !this._initializing) {
        this._initializing = true;
        window.setTimeout(Function.createDelegate(this, this._doInitialize), 0);
    }
}

Then inside the _doInitialize method:

  • Sys.Component.initialize() is called. (Sys.Component is the base class of Sys._Application)
  • init event handlers are invoked.
  • calls raiseLoad.
function Sys$_Application$_doInitialize() {
    Sys._Application.callBaseMethod(this, 'initialize');
    var handler = this.get_events().getHandler("init");
    if (handler) {
        this.beginCreateComponents();
        handler(this, Sys.EventArgs.Empty);
        this.endCreateComponents();
    }
    this.raiseLoad();
    this._initializing = false;
}

In raiseLoad method:

  • load event handlers are invoked.
  • pageLoad() function is invoked. (If one exists!)
function Sys$_Application$raiseLoad() {
    var h = this.get_events().getHandler("load");
    var args = new Sys.ApplicationLoadEventArgs(Array.clone(this._createdComponents), !this._initializing);
    if (h) {
        h(this, args);
    }
    if (window.pageLoad) {
        window.pageLoad(this, args);
    }
    this._createdComponents = [];
}

Problem!

Sys.Application.initialize uses the setTimeout trick to defer initialisation until the DOM is ready, but many have discovered and documented (see links at the end of the post) that using setTimeout cannot guarantee DOM readiness!

Calling initialize prematurely (i.e. before the DOM is ready), means that DOM elements may not have completely initialized and have their states restored (IE/Safari), this can lead to a whole load of issues when manipulating those elements, especially after a user clicks on the browser’s back button.

If Sys.Application.initialize() was not explicitly called, but invoked when the window.onload event is raised, DOM elements would have initialized and their (selection) states restored. But the window.onload event is only raised after all resources have been loaded, which is not ideal.

Now that Microsoft is shipping and supporting jQuery, I suggest using the “document is ready” event:

YourControl.prototype = {
    initialize: function() {
        $(document).ready(Function.createDelegate(this, this._onDocumentReady));
    },
    _onDocumentReady: function() {
        ... your initialize code ...
    },
    ... rest of your code ...
}

After a quick search, heaven knows why I didn’t do this first, I found these related posts that also highlight the problems and offer possible solutions:

Written by tzkuei

October 17, 2008 at 10:42 pm

Creating an asynchronous upload extender

leave a comment »

There have been many takes on the asynchronous upload control, but I am presenting another solution that will extend any existing <asp:FileUpload> control.

The basic mechanism to asynchronous upload is to submit the form to a ‘hidden’ iframe:

<html>
<body>
<form action="http://www.this-page-intentionally-left-blank.org/" method="get" target="winUpload">
	<input type="submit" value="Upload" />
</form>

</body>
</html>

In this example, I have declared the <iframe> in the mark-up, and set the form target to the <iframe>.  On clicking the upload button, you should see a “blank” page in the <iframe>.  This example works in all major browsers.

Since my goal is to create an extender, I need to create the <iframe> and hook up the form taget programmatically using DOM scripting:

<html>
<body>
<form action="http://www.this-page-intentionally-left-blank.org/" method="get">
	<input type="submit" value="Upload" />
</form>
<script>
var iframeId = "winUpload";
var iframe = document.createElement("iframe");
iframe.id = iframeId;
iframe.name = iframeId;
document.body.appendChild(iframe);
document.forms[0].target = iframeId;
</script>
</body>
</html>

This example worked in all major browsers except Internet Explorer! With IE, the form is submitted to a new page/tab instead of the <iframe> as specified in the target attribute.

Suspecting that may be the id and name attributes hadn’t been set correctly, I added Sys.Debug.trace(document.getElementById(“winUpload”)) and Sys.Debug.trace(document.getElementsByName(“winUpload”) and the console output shows the <iframe> element, suggesting that the <iframe> was created, attributes set and added to the DOM.

So why can’t the form find its target window? I looked up this problem in a search engine and found a handful of posts:

Using the suggestions from these posts, here is the updated example:

<html>
<body>
<form action="http://www.this-page-intentionally-left-blank.org/" method="get">
	<input type="submit" value="Upload" />
</form>
<script>
var iframeId = "winUpload";
if (document.all) {
    var iframe = document.createElement("");
} else {
    var iframe = document.createElement("iframe");
}
iframe.id = iframeId;
iframe.name = iframeId;
document.body.appendChild(iframe);
document.forms[0].target = iframeId;
</script>
</body>
</html>

B.t.w. If you are not using ASP.NET AJAX, why not take a look at YUI’s Connection Manager which implements asynchronous file uploads.

!!! To be continued !!!

Written by tzkuei

October 12, 2008 at 4:11 pm

Posted in DOM, HTML, JavaScript