Async file upload with MVC4
Uploading file asynchronously using JQuery and MVC4
Uploading files with form submit on web page, especially large file requires user to wait for browser until file is uploaded. This does not produce very nice user experience as user needs to wait without any notification for file to be uploaded. In case file is to large (if not validated on client), user will land to 500 page.
Imagine you had lat's say 12 fields to populate which you did and you accidentally chose larger file. You will have to re-enter those fields again and that is how you fail to enforce your client to use your service.
In this article I'll show how to overcome this issue by synchronously upload a file using JQuery and async WebAPI controller.
First we are going to start by creating new MVC4 Web Application.
After choosing "ASP.NET MVC 4 Web Application", we are going to choose "Empty" template to avoid unecessary files and settings for this sample web application.
Now we have our working place and we are set to write some code.
We'll first start by setting up template (master) view and default view and a controller for start page. Since these are elementary MVC things, I'm not going to waste your screen space and you can find these files in a whole solution for this sample web application.
We are going to create default view structure with form first
<h2>Async file upload</h2><form action="/api/FileUpload" method="post" enctype="multipart/form-data"><div id="uploadControls"><div> <span>Select file(s) to upload :</span> <input id="fileUpload" type="file" multiple="multiple" /> </div> <div> <input id="btnUpload" type="button" value="Upload" /> </div> <ul id="uploadResults"> </ul> </div> <div id="uploadProgress" class="hidden"> <img src="/images/ajax-loader.gif" alt="" /> </div> </form>
This form will be used to pickup file from user but we are not going to post it directly to our back-end. Instead of that, we are only going to use file input to let user pick the file, and we are going to send data via JQuery ajax call.
Content of page is split into two main div tags and one of them (uploadProgress) applies CSS class hidden which hides it initially from the page. This is done to show different set of messages when picking and uploading files.
$(document).ready(function () { $("#btnUpload").click(OnUpload); }); function ShowUploadControls() { $("#uploadControls").show(); $("#uploadProgress").hide(); } function ShowUploadProgress() { $("#uploadControls").hide(); $("#uploadProgress").show(); } function OnUpload(evt) { var files = $("#fileUpload").get(0).files; if (files.length > 0) { ShowUploadProgress(); if (window.FormData !== undefined) { var data = new FormData(); for (i = 0; i < files.length; i++) { data.append("file" + i, files[i]); } $.ajax({ type: "POST", url: "/api/FileUpload", contentType: false, processData: false, data: data, success: function (results) { ShowUploadControls(); $("#uploadResults").empty(); for (i = 0; i < results.length; i++) { $("#uploadResults").append($("<li/>").text(results[i])); } }, error: function (xhr, ajaxOptions, thrownError) { ShowUploadControls(); alert(xhr.responseText); } }); } else { alert("Your browser doesn't support HTML5 multiple file uploads! Please use some decent browser."); } } }
Now when we are done with client side we need to create an API controller named FileUpload since we are posting data in our script to /api/FileUpload path. Controller needs to have only POST method action since we are going to post file to this controller. Also as it is an async controller return type will be Task.
To read multipart data we need to use MultipartFormDataStreamProvider class but since we want to control where we are going to save the file we are going to create our custom provider which inherits MultipartFormDataStreamProvider and we are going to override it's method GetLocalFileName to return file path where we are going to store the file.
public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider { public CustomMultipartFormDataStreamProvider(string path) : base(path) { } public override string GetLocalFileName(System.Net.Http.Headers.HttpContentHeaders headers) { string fileName; if (!string.IsNullOrWhiteSpace(headers.ContentDisposition.FileName)) { fileName = headers.ContentDisposition.FileName; } else { fileName = Guid.NewGuid().ToString() + ".data"; } return fileName.Replace("\"", string.Empty); } }
Now we are going to use our custom provider to read posted data in async controller
public class FileUploadController : ApiController { public Task<IEnumerable<string>> Post() { /*throw new Exception("Custom error thrown for script error handling test!");*/ if (Request.Content.IsMimeMultipartContent()) { /*Simulate large file upload*/ System.Threading.Thread.Sleep(5000); string fullPath = HttpContext.Current.Server.MapPath("~/Uploads"); CustomMultipartFormDataStreamProvider streamProvider = new CustomMultipartFormDataStreamProvider(fullPath); var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t => { if (t.IsFaulted || t.IsCanceled) throw new HttpResponseException(HttpStatusCode.InternalServerError); var fileInfo = streamProvider.FileData.Select(i => { var info = new FileInfo(i.LocalFileName); return "File saved as " info.FullName " (" info.Length ")"; }); return fileInfo; }); return task; } else { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "Invalid Request!")); } } }
It's all set up and we can test solution now. Just for testing purposes there is a line for putting thread to sleep for 5 seconds just for testing purposes so we can see progress bar animation.
And the result is displayed after file upload.
Complete solution you can find attached to this article
Attached solution is size of around 2MB because of package files downloaded by NuGet package manager. It has not been cleaned up for the easier run right after download
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article