ASP.NET File Upload with *Real-Time* Progress Bar
I had a number of people email me asking how the ProgressBar Toolkit control I blogged about last month could be used to provide progress for file uploads. So I thought I would spend some time and see how this could be done. Thankfully, there are a ton of resources available on the internet that discuss this in pretty good detail. But I didn't see any good examples of how to include the real-time count of the number of bytes that have been transferred. So I thought I would try to tackle that problem while building my example.
Update 7/27/2008: This post is bogus - I have over simplified the problem. My goal was to display the number of bytes that have been transferred to the web server, not the number that have been saved to disk. As atashbahar pointed out, this will require a little bit more work that what I have shown. I followed atashbahar's suggestion and downloaded the NeatUpload control (its free) and started taking a look at its HttpModule. I am going to take a look at how the NeatUpload handles uploading large files and will update this post or create a part II that outlines what I have found.
Also, a couple of quick notes on the demo. The first is that the demo looks like crap in FF, sorry but I gave up styling the input element. I will come back to it later. Second, I have a limited amount of bandwidth that I can use each month, so I don't accept files larger than 250 bytes. I recommend creating a real small text file and using that to upload to the demo site. I have setup the demo to upload only a handful of bytes per second, so you will be able to get a feel for how the progress bar works even with real small files.
Basic Approach
A few minutes of googleing and you will learn that creating the file upload widget above includes the following pieces ...
- A input element for collecting the file. I am using the ASP.NET FileUpload control to render this.
- 2 ASP.NET pages. One contains the FileUpload control and has codebehind logic that reads the file from the response and writes it some place. And a second page that contains an IFRAME with the src attribute that points to the upload page.
- A UI widget for displaying the upload's progress. I have used the progress bar control I have blogged about previously
The general idea is that when the Upload button is clicked, a bit of JavaScript runs that causes the form that contains the FileUpload control to submit. Because just the nested form is submitting the main content page remains unchanged (i.e. it isn't refreshed).
Here is the markup for the Default page
1: <form id="form1" runat="server">
2: <div>
3: <div class="upload">
4: <h3>File Upload</h3>
5: <div>
6: <iframe id="uploadFrame" frameborder="0" scrolling="no" src="Upload.aspx"></iframe>
7: <mb:ProgressControl ID="progress" runat="server" CssClass="vista" style="display:none" Value="0" Mode="Manual" Speed=".4" Width="100%" />
8: <div>
9: <div id="status" class="info">Please select a file to upload</div>
10: <div class="commands">
11: <input id="upload" type="button" value="Upload" />
12: </div>
13: </div>
14: </div>
15: </div>
16: </div>
17: </form>
And here is the Upload page
1: <form id="form" runat="server" enctype="multipart/form-data">
2: <div>
3: <asp:FileUpload ID="fileUpload" runat="server" Width="100%" />
4: </div>
5: </form>
Posting the File - Building the Upload.aspx Page
This is the complicated part of this implementation. When the Upload.aspx page is submitted, I want to take the uploaded file off of the response and save it to disk. But, because I want to display the real-time progress, I need a way to get at how much of the uploaded file has been transferred. In my first crack at solving this, I tried just calling SaveAs on the HttpPostedFile object and then using a regular WebMethod that would return the size of the file on disk. Something like this ...
1: protected void SaveUploadedFile(string filePath)
2: {3: // save the file
4: this.fileUpload.PostedFile.SaveAs(filePath);
5: } 6: 7: public int GetFileSize(string filePath)
8: {9: // get the file size on disk
10: return new FileInfo(filePath).Length;
11: }
But, that wasn't working too well because the file was being buffered as it was being written to disk so my progress bar would jump from 0 to 67 to 100. Which wasn't what I wanted. So to get around this, I decided that I would try handling saving the file myself. To do this I manually read X bytes off the response stream and write it to a FileStream (where X is the size of the buffer). For the demo I have set X to 1, so each byte is read/written individually, but for non-demo purposes I believe I will be updating the size of the buffer based on the size of the file being uploaded. The smaller the buffer size, the longer the file takes to upload, but the more accurate our progress bar is. And of course, a large buffer will upload faster, but our progress bar won't be as accurate.
1: // DEMOWARE: set the buffer size to something larger.
2: // the smaller the buffer the longer it will take to
3: // download, but the more precise your progress bar will be.
4: // to large of a value and the progress bar will make real large jumps
5: int bufferSize = 1;
6: byte[] buffer = new byte[bufferSize];
7: 8: // get the status object from session
9: UploadInfo uploadInfo = this.Session["UploadInfo"] as UploadInfo;
10: 11: // write the byte to disk
12: using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.Create))
13: {14: // aslong was we haven't written everything ...
15: while (uploadInfo.UploadedLength < uploadInfo.ContentLength)
16: {17: // fill the buffer from the input stream
18: int bytes = this.fileUpload.PostedFile.InputStream.Read(buffer, 0, bufferSize);
19: // write the bytes to the file stream
20: fs.Write(buffer, 0, bytes);21: // update the number the webservice is polling on
22: uploadInfo.UploadedLength += bytes; 23: } 24: }
Checking the Progress
Back on the main page (Default.aspx), I have setup a Page Method that retrieves the number of bytes that have been written to the FileStream as well as a friendly status message that reports the total number of bytes that have been transferred so far.
1: [System.Web.Services.WebMethod] 2: [System.Web.Script.Services.ScriptMethod]3: public static object GetUploadStatus()
4: {5: // get the length of the file on disk and divide that
6: // by the length of the stream ...
7: UploadInfo info = HttpContext.Current.Session["UploadInfo"] as UploadInfo;
8: 9: if (info != null && info.IsReady)
10: {11: int soFar = info.UploadedLength;
12: int total = info.ContentLength;
13: 14: int percentComplete = (int)Math.Ceiling((double)soFar / (double)total * 100);
15: string message = string.Format("Uploading {0} ... {1} of {2} Bytes", info.FileName, soFar, total);
16: 17: // return the percentage
18: return new { percentComplete = percentComplete, message = message };
19: } 20: 21: // not ready yet ...
22: return null;
23: }
And back on the client, I have setup a bit of JavaScript that calls the GetUploadStatus PageMethod every second or so. When the status changes, the script updates the progress bar to the new value and updates the status text.
1: // start polling to check on the progress ...
2: var intervalID = window.setInterval(function(){
3: // call the GetUploadStatus Page Method
4: PageMethods.GetUploadStatus(function(result){
5: if(result){
6: // update the progressbar to the new value
7: progressBar.set_percentage(result.percentComplete);8: // upadte the message
9: updateMessage('info', result.message);
10: 11: if(result == 100){
12: // clear the interval so we stop polling
13: window.clearInterval(intervalID); 14: } 15: } 16: }); 17: }, 500);
Conclusion
I am not using this in production (yet!). So if you find some issues with it, just leave a comment or drop me an email. And there is still a bunch that can be approved (like supporting multiple files), so if you make any mods to this, let me know that too so I don't have to redo any of your hard work ;)
That's it. Enjoy!
Comments
So good. I've been run your demo, and it run smoothly. Thanks for your great sample.
I think there is a problem with this. ASP.NET engine first caches the whole request (that includes the uploaded files) and then processes them. This means you won't reach the code that processes the upload until upload is finished (this is more clear when you upload bigger files).
One way to have real time progress is to write a HTTP Module that handles the request directly and parses it for files. It is not a very easy thing to do though.
There are tons of commercial upload controls out there but personally I like NeatUpload (http://www.brettle.com/neatupload) because it is open source and free. NeatUpload has a great module for handling uploads.
Another option is to use flash for uploads. There is a great component named SWFUpload (http://swfupload.org/) for this matter (this is free too).
You strong, and support you.
@atashbahar -
Arghh ... My solution doesn't work to well then. I am going to take a peak at NetUpload and see how that control works.
Thanks for pointing this out.
Matt.
By the same token, you are not really limiting the size of the file they can upload. You are limiting the size of file they can save to the server disk. The request size restriction is too low-level for ASP.NET to handle programmatically, but there's a web.config setting under system.web (httpRuntime maxRequestLength) to set the maximum, in kilobytes. When they go over this specified limit, they get cutoff by IIS, and end up seeing a "Page cannot be displayed".
Sorry for trying to upload a 700MB file, by the way :)
Regards...
I've looked at a few different controls:
http://remy.supertext.ch/2008/01/file-upload-with-aspnet/
and NeatUpload came out as the winner, because it was one of the few controls that really worked in every browser. We use it in Production since about 3 months now and had no issues with it so far.
It does have a few drawbacks though:
- It does make a page refresh at the beginning and at the end, so it's not as sexy as it could be
- Only classic mode on IIS 7
And it's not using ASP.NET Ajax, not a big deal, but it does put a lot of javascript into the page.
@Josh -
Thanks Josh. I updated the web config. Boy, ever had a post that you wish you hadn't written? This is one of them for me ;) I severly misunderstood this problem!
I did look into the NeatUpload though and I am intrigued about all of the stuff going on. I think I am going to spend sometime and see if I can integrate with the HttpModule it is using or create a new module based on this one.
@Remy -
Thats good info to know, cuz I am going to have do something like this in production as well. After I spend some time with the NeatUpload module, hopefully I can address some of these issues.
One big problem with all Upload tools is that they only work in classic Mode in IIS 7. So far the only workaround seems to be to hack into this stuff with Reflection. There is a big discussion on:
http://www.brettle.com/ForumThreadView.aspx?thread=441&pageindex=7
Good luck :-)
There is a nice File Upload HttpModule available (source available) on
http://darrenjohnstone.net/2007/06/02/writing-an-aspnet-file-upload-module/
The module works in conjunction with a special HttpHandler to report on the progress of the upload.
On top of that, the post is clear, very well written, and includes ample technical information on the details of the implementation.
Mix both together and you've got one powerfull uploader.
BTW: your upload webcontrol creates an iframe that references upload.aspx; why not implement upload.aspx as a Custom HttpHandler as well, that way you don't have to bother with the page and everything can be included in one dll.
How execute in VS 2005 ?
Dont work
Demo is working when i run thru visual studio (vs development web server).
But, when i publish it in local - Vista 64 SP1 (IIS 7)
it's not working....
its just says ... initlizing... and upload button got disabled and nothing happens afterwords.
thanks,
krunal
Thank you your demo is awesome!
Keep up that creativity and good work!
Your work is great!
I just have one question. If I move the Upload.aspx and Default.aspx files away from the root folder... I get this error:
The type or namespace name 'upload_aspx' could not be found (are you missing a using directive or an assembly reference?)
Is it possible to do this or do all the files and folders have to be in the same root directory as the Bin folder?
Thanks,
Albert
Hi, Matt!
Did you know how to find the way to catch exception on FileUpload ctrl, then the name of the file is like 'dasfasdgf'?
I have a page with tab 'Upload' and 'Main'.
Then i wrote some name on keyboard, like 'dasfasdgf' and after this choose tab 'Main' i have exception:
"htmlfile: Access is denied"
Any ideas?
is there any demo for multiple file upload in asp.net
How can use dll of materberseth ajax control how to embed controls of this ajaxtool kit, How to download ajax tool kit
Buffered file download using asp.net Buffered File Download from Asp.Net Web Server.
HI
I am having this error "Error 1 Child nodes not allowed. (c:\inetpub\wwwroot\webroot\web.config line 79)"
when trying to run the code can u help please i am using .Net 2.0 thanks.