/dev/null [tony lombardo]

Anything and everything ASP.NET and more. Expect to see tips and tricks, opinions on new technology, and fun code samples, along with the occasional rant.
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

  1. Add the file upload control, or html input element to your form.
    <input type='file' id='upload' name='upload' />
  2. Add an iframe to your page
    <iframe src='blank.htm' name='hiddenFrame'></iframe>
  3. 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
  4. Define the submitForm function as follows:
  5. //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();
    }

  6. Add a button to your page which calls the submitForm function.
    <button onclick="[javascript]:submitForm('hiddenFrame','upload')" >Upload</button>
  7. 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.

Posted: 09 Apr 2007, 17:52
Filed under: ,

Comments

Mike Brengartner said:

Thanks for this info. I'd love to see more about the other things you talked about at the end of the article.  I hope you get some free time!

Thanks,

Mike

# April 17, 2007 4:39 PM

Mike Brengartner said:

Also, the iframe seems to open up a new browser window.  Any way around that?

Thanks again,

Mike

# April 20, 2007 11:08 AM

Tony Lombardo said:

Hi Mike - In all of my testing, the form submit didn't force a new window to open. The only happened when I attempted to dynamically create an iframe and insert it into the DOM.  It appears that the browser doesn't like adding IFrame's to the list of named frames when done with Javascript through the DOM.  

# April 20, 2007 12:31 PM

Mike Brengartner said:

Thanks for the reply.  I've enclosed my code (which is really just yours from above).  When I run this and hit "upload" a new tab opens in ie 7 and i get two copies of the form.  Any idea what's happening?

the iframe.htm is an empty html file.

Thanks,

Mike

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="SimpleAjaxProgressIndicator.aspx.cs" Inherits="SimpleAjaxProgressIndicator" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

   <title>Untitled Page</title>

   <link href="default.css" rel="stylesheet" type="text/css" />

   <script type="text/javascript">

   function submitForm(frameName, upload)

   {

       divLoading.style.visibility = "visible";

       //document.forms[0].action = "SimpleAjaxProgressIndicator.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>

   <form id="form1" runat="server" enctype="multipart/form-data">

   <div>

       <asp:FileUpload ID="FileUpload1" runat="server" />

       <br />

       <button onclick="BLOCKED SCRIPTsubmitForm('hiddenframe', 'FileUpload1');">Upload</button>

       <div id="divLoading" style="visibility:hidden">

           <img src="circle-ball-dark-antialiased.gif" /> Loading...

       </div>

   </div>

   <iframe id="hiddenframe" src="iframe.htm" scrolling="no" style="DISPLAY: inline; BORDER-TOP-STYLE: none; BORDER-RIGHT-STYLE: none; BORDER-LEFT-STYLE: none; BORDER-BOTTOM-STYLE: none" frameborder="0"></iframe>

   </form>

</body>

</html>

# April 20, 2007 3:39 PM

Tony Lombardo said:

Unfortunately, you're stuck using the "name" attribute on the IFrame, if you want the browser to load it into the frames collection.  In your code above, add name="hiddenframe"

# April 20, 2007 3:51 PM

amy said:

is this can be done by asp? please help

# July 17, 2007 1:57 AM

Tony Lombardo said:

Sure - this technique can be used with any server page technology.  The majority of the code is client-side javascript.  The only portion you would need to write the ASP code to handle, is the file upload itself.

# July 17, 2007 9:40 AM

access the file from server side code said:

hi,

but how do you access the file from server side code? to do server side validation and actually save the file in server?

# July 27, 2007 4:29 AM

Tony Lombardo said:

That depends on the server platform you're using.  If you're using ASP.NET, you can use the Files collection which is part of the request object.  

# August 1, 2007 9:27 AM

Dan said:

I tried using this on an asp.net user control, and it stopped everything under the iframe on my page (the upload and delete button, other user controls on the page) from rendering.  Have you ever seen that before?

code:

<asp:FileUpload ID="FileUpload1" runat="server" />

<iframe src="blank.htm" name="ifHidden" style="display:none" />

<br />

<button>Upload</button>

&nbsp;&nbsp;<asp:Button ID="btnDelete" runat="server" OnClick="btnDelete_Click" Text="Delete" CausesValidation="false" />

Thanks,

Dan

# September 6, 2007 2:01 PM

Tony Lombardo said:

Dan, you might want to check and see if the HTML for the buttons, etc is actually being rendered.  Is it possible that there's a missing closing tag, or perhaps an unclosed ""?  That could cause the browser to get confused when rendering.

# September 10, 2007 1:26 PM

Mikhail said:

How do we track the end of upload request?

Thanks in advance!

# November 14, 2007 10:02 AM

Tony Lombardo said:

Mikhail - there are a couple of ways that I can think of.  You should be able to use the ReadyState of the IFrame window to check if the request/response has completed.  You can also set up a separate polling service which would check on the state of the request/response.

# November 15, 2007 9:32 AM

Jeremy Regan said:

Dan, you have to close the iframe tag with a real close tag, like </iframe> you can't just use the /> syntax because it stupidly cannot tell that you are trying to make a standalone tag.

# December 17, 2007 3:12 PM

rpr said:

Hi,

Thanks for u'r coding. I have substituted input file control to asp.net file upload control.

And in the page_load i have added the folowing coding

if (myFile.HasFile)

   {

       string strFileName;

       int intFileNameLength;

       string strFileExtension;

       strFileName = myFile.FileName;

       intFileNameLength = strFileName.Length;

       strFileExtension = strFileName.Substring(intFileNameLength - 4, 4);        

                 try

           {

               myFile.PostedFile.SaveAs(Server.MapPath(".") + "//Upload//" + strFileName);

               lblMsg.Text = strFileName + " Uploaded successfully!";

           }

           catch (Exception exc)

           {

               lblMsg.Text = exc.Message;

           }

         }

but the page seems to be refreshed fully..

Where am i going wrong???

# January 3, 2008 7:39 AM

Tony Lombardo said:

rpr - sounds like the form submit is happening on the main form, or in the same window of your main page.  The standard .net fileupload control does not work asynchronously, so it will cause a postback.

# January 30, 2008 9:49 AM

Lucio said:

Wow, I just implemented this. It's a wonderful idea and quite ninja, too! Thank you so much!!!

# April 3, 2008 4:37 AM

Padmakumar said:

How can I track the progress of the file upload? Like I wanted to show messages  "10% Uploaded", "50% uploaded", etc...

# April 30, 2008 11:36 AM

pixel3cs said:

I have tried everything to make this work with ASP .NET but no luck. The postback does not work asynchronously. The Default.aspx is refreshed too, even after i have set document.forms[0].action = "UploadPage.aspx";, and others problem appear too.

My idea was to make a control that will also show a progress bar. When begin to upload the file on the server a session variable will keep the progress value, and using ajax calls read that session variable and show the progress in the browser window. This is great for large files or for slow connections, to know the progress of the uploading task.

But it seems that when you POST a file to the server the file is first uploaded to a temp folder on the server and then you get a fileUpload.PostedFile.InputStream to that temp file and not to a live stream from where you can read bytes, so showing a progress is impossible.

Is there a way to ask the iframe/browser what is the progress when the data is uploaded on the server? I guess not, that's why i am looking to use Flash for upload files and show progress. With XBAP things are very simple, you can read files and using webservices upload the data to the server and you can very easy show a progress bar.

The problem when use POST from inside a browser to upload a file is that you don't have read access to the file hosted on user hdd.

# May 1, 2008 11:55 AM

Tony Lombardo said:

@Padmakumar - Tracking the tracking the progress would need to be done on the server side, much the same way that pixel3cs has tried.  

@pixel3cs - There's no way to get the progress of the file upload on the client-side (not without writing a browser plugin/ActiveX control).  A form post can put the complete file-size in the request header if available. However, not all files have this information available.  A BMP is an example of a file where the total size isn't known until the entire file has been transmitted.  Bummer, I know.. Unfortunately, I don't think there's any way to work around this without breaking the browser sandbox confines.  

# May 12, 2008 9:38 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS