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.

New 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.

Empty Template

Now we have our working place and we are set to write some code.

Note

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.

Controls

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.

Progress

And the result is displayed after file upload.

Result

Complete solution you can find attached to this article

Note

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.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article