File Uploads - Where's the Ajax?
As with everything else Web related these days, wouldn't file uploads be better with Ajax? That's what most of us tend to think, but the devil is in the details! The short answer is, you cannot do the actual file transfer through an XMLHTTP request. (Why don't file uploads work during async postbacks?) The long answer is, you can still use Ajax to make it better.
Background
In order to understand why Ajax can't help us here, we have to understand how a normal Ajax request works. Unlike a native form post, when we craft an Ajax request, the post data must be manually set through JavaScript. For instance, your typical Ajax request will put together a string like "view=2&category=6", which will be sent as the post data. In a more sophisticated framework, a function will go through and assembly the post data by inspecting each form field.
Problem
An input field with type='file' can not be programmatically set. The reason.. security. Suppose you could construct an input filed and set the value through JavaScript. If such a scenario were possible, any website would be able to access files on your local machine at their will. Understandably browsers prohibit programmability of such fields.
An Ajax Alternative?
We've all embraced Ajax as a revolutionary technology, but many of us forget (or are not aware) of asynchronous posts in the times before XmlHttp requests. Our good old friend the IFrame used to be the preferred option for asynchronous http communications. Because an iframe is in essence it's own browser window, it can be used to fire off asynchronous requests (both POST and GET). However, even more important is an IFrame's ability to be a 'target' of a form POST. By adding an IFrame to the page and setting it as the target of the form post, you can in essence create an asynchronous file transfer.
The Details
- Add the file upload control, or html input element to your form.
<input type='file' id='upload' name='upload' />
- Add an iframe to your page
<iframe src='blank.htm' name='hiddenFrame'></iframe>
- Add a 'blank.htm' page to your solution
This page is used to ensure the IFrame is scoped in the same site as your page - otherwise the browser would throw an access violation error when attempting to script the IFrame's contents
- Define the submitForm function as follows:
//Our function expects 2 parameters, the name of the disposable frame
//that we will use for the form post, as well as the id of the input control used to upload the file.
function submitForm(frameName,upload){
document.forms[0].action="default.aspx"
//The magic line.. set the target of the form post to be our hidden IFrame
document.forms[0].target=frameName;
//We have to use a setTimeout here so that we can update the document in a separate thread
//otherwise the document wouldn't update until after the upload completed.
window.setTimeout(function(){
var uploadE=document.getElementById(upload);
uploadE.parentElement.appendChild(document.createTextNode(uploadE.value));
uploadE.parentElement.replaceChild(uploadE.cloneNode(true),uploadE);
},100);
document.forms[0].submit();
}
- Add a button to your page which calls the submitForm function.
<button onclick="[javascript]:submitForm('hiddenFrame','upload')" >Upload</button>
- Test it out!
In the steps above, we set up the page so that when our special "Upload" button is clicked, we redirect the response to the hidden iframe. By doing this, we can free our main page from processing the response, in essence making the upload occur asynchronously. Additionally, we can have simultaneous uploads occur in different 'threads' by creating a separate IFrame for each input control. Below is an example of a page which contains 3 upload fields, that can be used simultaneously.
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
<script type="text/javascript">
function submitForm(frameName,upload){
document.forms[0].action="default.aspx"
document.forms[0].target=frameName;
window.setTimeout(function(){
var uploadE=document.getElementById(upload);
uploadE.parentElement.appendChild(document.createTextNode(uploadE.value));
uploadE.parentElement.replaceChild(uploadE.cloneNode(true),uploadE);
},100);
document.forms[0].submit();
}
</script>
</head>
<body>
<iframe name="hiddenFrame" src="blank.htm" mce_src="blank.htm" style="display:none"></iframe>
<form id="form1" runat="server" enctype="multipart/form-data" >
<div id="Div1">
<input type="file" name="upload" id="upload" />
<button onclick="[javascript]:submitForm('hiddenFrame','upload')">Upload</button>
<iframe name="hiddenFrame" src="blank.htm" mce_src="blank.htm" style="display:none"></iframe>
</div>
<div id="uploadContainer">
<input type="file" name="upload1" id="upload1" />
<button onclick="[javascript]:submitForm('hiddenFrame1','upload1')">Upload</button>
<iframe name="hiddenFrame1" src="blank.htm" mce_src="blank.htm" style="display:none"></iframe>
</div>
<div id="uploadContainer2">
<input type="file" name="upload2" id="upload2" />
<button onclick="[javascript]:submitForm('hiddenFrame2','upload2')">Upload</button>
<iframe name="hiddenFrame2" src="blank.htm" mce_src="blank.htm" style="display:none"></iframe>
</div>
</form>
</body>
</html>
What's Next?
This is just the beginning of what could be a really useful FileUpload control. We took care of the major hurdle - we've freed the page of the postback chains that were associated with your typical HTML Form Upload process. The UI could use some work.. and the code should be put into a custom WebControl instead of manually adding the contents - but that's all just icing on the cake. One thing I want to do once I get some time, is to hook up a window.onload listener to the IFrame and disable/enable the upload field based on the status of the file transfer. I also think there's a way to get a progress indicator working with a little creative Ajax - but I'll save that for another day.