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
- <form id="form1" runat="server">
- <div class="upload">
- <h3>File Upload</h3>
- <iframe id="uploadFrame" frameborder="0" scrolling="no" src="Upload.aspx"></iframe>
- <mb:ProgressControl ID="progress" runat="server" CssClass="vista" style="display:none" Value="0" Mode="Manual" Speed=".4" Width="100%" />
- <div id="status" class="info">Please select a file to upload</div>
- <div class="commands">
- <input id="upload" type="button" value="Upload" />
- </div>
- </div>
- </div>
- </div>
- </div>
- </form>
And here is the Upload page
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 ...
- protected void SaveUploadedFile(string filePath)
- {
- // save the file
- this.fileUpload.PostedFile.SaveAs(filePath);
- }
- public int GetFileSize(string filePath)
- {
- // get the file size on disk
- return new FileInfo(filePath).Length;
- }
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.
- // DEMOWARE: set the buffer size to something larger.
- // the smaller the buffer the longer it will take to
- // download, but the more precise your progress bar will be.
- // to large of a value and the progress bar will make real large jumps
- int bufferSize = 1;
- // get the status object from session
- UploadInfo uploadInfo = this.Session["UploadInfo"] as UploadInfo;
- // write the byte to disk
- {
- // aslong was we haven't written everything ...
- while (uploadInfo.UploadedLength < uploadInfo.ContentLength)
- {
- // fill the buffer from the input stream
- int bytes = this.fileUpload.PostedFile.InputStream.Read(buffer, 0, bufferSize);
- // write the bytes to the file stream
- fs.Write(buffer, 0, bytes);
- // update the number the webservice is polling on
- uploadInfo.UploadedLength += bytes;
- }
- }
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.
- [System.Web.Services.WebMethod]
- [System.Web.Script.Services.ScriptMethod]
- public static object GetUploadStatus()
- {
- // get the length of the file on disk and divide that
- // by the length of the stream ...
- UploadInfo info = HttpContext.Current.Session["UploadInfo"] as UploadInfo;
- if (info != null && info.IsReady)
- {
- int soFar = info.UploadedLength;
- int total = info.ContentLength;
- int percentComplete = (int)Math.Ceiling((double)soFar / (double)total * 100);
- string message = string.Format("Uploading {0} ... {1} of {2} Bytes", info.FileName, soFar, total);
- // return the percentage
- }
- // not ready yet ...
- return null;
- }
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.
- // start polling to check on the progress ...
- var intervalID = window.setInterval(function(){
- // call the GetUploadStatus Page Method
- PageMethods.GetUploadStatus(function(result){
- if(result){
- // update the progressbar to the new value
- progressBar.set_percentage(result.percentComplete);
- // upadte the message
- updateMessage('info', result.message);
- if(result == 100){
- // clear the interval so we stop polling
- window.clearInterval(intervalID);
- }
- }
- });
- }, 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!
1 comment.
The Lifehacker Editors’ Favorite Software and Hardware [What We Use] »« MySQL performance tuning
