Drag and Drop asynchronous file upload with HTML 5, File API, XMLHttpRequest and Asp.Net MVC 3

Standard

Heads up!

The blog has moved!
The new URL to bookmark is http://blog.valeriogheri.com/

 

Hello everyone, today I’m going to talk about Drag ‘n Drop in modern web applications (someone said HTML5?).
I got interested in this topic because of a project I’m currently working on: we have a page where the user has to upload an Excel file to load some info into the system and instead of having just a page with the old-fashioned input for browsing the filesystem to pick a file and then press submit, we decided it would have been fun & useful to allow the user to drag and drop the desired file into the page, similar to what you can do with Gmail attachments. After googling, I found out that HTML 5 draft specification includes some new cool features that allow us to easily create a system that gets the job done in a pretty straightforward way. There are already a good number of (jQuery and non) plugins out there that do get the job done (in a much more clever way than my demo does), but I think it can be good fun and very useful to learn how to do it yourself first.

Goal

I’m going to showcase a simple Asp.Net MVC 3 web app that lets a user drag one image file at a time from the desktop and drop it into a special “drop zone” into the web page. The system will then take care of uploading it to the server and, if upload was successfull, it will display to the user the link to access the uploaded image.
As always all the source code is hosted on github at the following URL: https://github.com/vgheri/HTML5Drag-DropExample

What I’ll use

  • Asp.Net MVC 3
  • HTML 5 DnD API, File API and XMLHttpRequest object to handle drag&drop and async file upload
  • jQuery (version 1.7.1 is the latest version at the moment)
  • Bootstrap for UI widgets

Important note

HTML5 and the FileAPI are still draft versions and as such they are subject to change until the final revision comes out and are not supported in all major browsers.
To see exactly what can be used and what not, take a look at the following two websites: http://caniuse.com/  http://html5please.us/.
All the code you will find below has been tested under Google Chrome 16 and IE 9.
Conceptually we can divide the async file upload operation in two different and consecutive moments:

  1. Drag and drop
  2. Async file upload and View update

Part 1: Drag and Drop

This part is all about creating a webpage with a really simple layout: just one dropbox (HTML5 DnD API) where the user can drop an image.
After dropping the image, the web page will load the dropped file in memory (using JS FileAPI) and will show to the user a table with a thumbnail, some basic information like filename and size and a progress bar to provide feedback to the user about the upload operation.
As you can see in the below screenshot, the page I’m going to build is really simple:
Landing page as displayed by Chrome

Landing page as displayed by Chrome

We can start creating something very simple but not that ugly using Bootstrap (I won’t show the Layout code here, you can download the source on github) and the Index page at the beginning looks something like this:

<form id="fileUpload" action="/Home/Upload" method="POST" enctype="multipart/form-data">

    <!-- Main hero unit for a primary marketing message or call to action dropzone="copy f:image/png f:image/gif f:image/jpeg" f:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet f:application/vnd.ms-excel-->
    <div id="dropbox" dropzone="copy f:image/png f:image/gif f:image/jpeg" class="hero-unit">
        <h2 id="droplabel">Drop zone</h2>
        <p id="dnd-notes">Only images file type are supported. Multiupload is not currentrly supported. Once you will drop your image in the dropzone, the upload will start.</p>
        <table id="files-list-table">
            <thead>
                <tr>
                    <th></th>
                    <th>File name</th>
                    <th>File size</th>
                    <th>Upload status</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td id="td-img-preview" class="tableData">
                        <img id="preview" src="" alt="preview will display here" />
                    </td>
                    <td id="td-file-name" class="tableData"></td>
                    <td id="td-file-size" class="tableData"></td>
                    <td id="td-progress-bar" class="tableData">
                        <div id="progressBarBox" class="progress progress-info progress-striped">
                          <div class="bar"></div>
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>

        <br/>
        <div id="multiupload-alert"class="alert alert-error">
            <h4 class="alert-heading">Warning!</h4>
            <span id="multiupload-alert-message"></span>
        </div>
        <input id="fileSelect" type="file" name="fileSelect" />
        <p><button id="fallback-submit" type="submit" class="btn btn-primary btn-large">Upload</button></p>
    </div>
</form>
We start to see the first HTML5 thing here with the “dropzone” attribute:
<div id="dropbox" dropzone="copy f:image/png f:image/gif f:image/jpeg" class="hero-unit">
As the draft specification says, it’s purpose is to tell the browser that this element will accept files Drag&Drop.
This attribute can be placed on every HTML element.
Its value specifies which action must be taken upon dropping the file and which MIME types are accepted.
Possible values are:

  1. copy
  2. move
  3. link
I used copy, which indicates that dropping an accepted item on the element will result in a copy of the dragged data, and this value is also the default behaviour.
The second part of the value, “f:image/png f:image/gif f:image/jpeg” indicates the accepted MIME types.
That being said, as of now the dropzone attribute is not currently supported in any of the major browsers, this means that we have to find another way to do it.
Luckily enough, some major browsers support the javascript events related to DnD, like:
drop, dragover or dragexit, so the “logic” will be handled using  javascript!
You can see I also added a classic input type=”file” along with a submit button:
<input id="fileSelect" type="file" name="fileSelect" />
        <p><button id="fallback-submit" type="submit" class="btn btn-primary btn-large">Upload</button></p>
This is there to provide compatibility whenever DnD or FileAPI or async upload (XMLHttpRequest object) are not supported by the browser, as it’s the case for IE9, so we are just fallin back to classic file upload and the user is still able to use the service.
Whenever a fallback is needed, the page displays like the below image
Landing page as displayed by IE9

Landing page as displayed by IE9

So, how do we handle the DnD events and how do we detect if the browser supports FileAPI and async upload?
<script type="text/javascript">
    var fileName = null;
    var fileToUpload = null;

    $(document).ready(function () {
        // init the widgets
        $("#progressBarBox").hide();
        $("#files-list-table").hide();

        // Check if FileAPI and XmlHttpRequest.upload are supported, so that we can hide the old style input method
        if (window.File && window.FileReader && window.FileList && window.Blob && new XMLHttpRequest().upload) {
            $("#fileSelect").hide();
            $("#fallback-submit").hide();
            $("#multiupload-alert").hide();
            var dropbox = document.getElementById("dropbox")
            // init event handlers
            dropbox.addEventListener("dragenter", dragEnter, false);
            dropbox.addEventListener("dragexit", dragExit, false);
            dropbox.addEventListener("dragleave", dragExit, false);
            dropbox.addEventListener("dragover", dragOver, false);
            dropbox.addEventListener("drop", drop, false);
        }
        else {
	$("#multiupload-alert").hide();
            $("#dnd-notes").hide();
        }
    });

    function dragEnter(evt) {
        evt.stopPropagation();
        evt.preventDefault();
    }

    function dragExit(evt) {
        evt.stopPropagation();
        evt.preventDefault();

        $("#dropbox").removeClass("active-dropzone");
    }

    function dragOver(evt) {
        evt.stopPropagation();
        evt.preventDefault();

        $("#dropbox").addClass("active-dropzone");
    }

continues...
The check happens here:
// Check if FileAPI and XmlHttpRequest.upload are supported, so that we can hide the old style input method
        if (window.File && window.FileReader && window.FileList && window.Blob && new XMLHttpRequest().upload)
The first 4 conditions are related to the JS File API, whereas the last is about the async upload. Then if the check is positive, we hide the classic file upload mode and we add event handlers for the dropbox to handle DnD
$("#fileSelect").hide();
$("#fallback-submit").hide();
$("#multiupload-alert").hide();
var dropbox = document.getElementById("dropbox")
// init event handlers
dropbox.addEventListener("dragenter", dragEnter, false);
dropbox.addEventListener("dragexit", dragExit, false);
dropbox.addEventListener("dragleave", dragExit, false);
dropbox.addEventListener("dragover", dragOver, false);
dropbox.addEventListener("drop", drop, false);
Important note: it is not sufficient to just add a handler for the drop event, as it will not properly fire if the others are not set (like WPF DnD events).
I use the dragover and dragexit handlers to modify the dropbox appearance.
Now on to the drop event handler:
function drop(evt) {
        evt.stopPropagation();
        evt.preventDefault();

        $("#dropbox").removeClass("active-dropzone");
        var temp = $("#multiupload-alert");
        // If there is an alert message displayed, we hide it
        $("#multiupload-alert").hide();
        var files = evt.dataTransfer.files;
        var count = files.length;

        // Only call the handler if 1 file was dropped.
        if (count == 1) {
            handleFiles(files);
        }
        else if (count > 1) {
            var message = "Multiupload is not currently supported. Choose only one file.";
            $("#multiupload-alert-message").html(message);
            $("#multiupload-alert").show();
        }
    }
We retrieve the dropped file in the following line of code
var files = evt.dataTransfer.files;
Then if the user dropped only one file, we can go on parsing
function handleFiles(files) {
        var file = files[0];
        if (file.type.match('^image/')) {
            $("#files-list-table").show();
            fileToUpload = file;
            fileName = file.name;
            var reader = new FileReader();

            // init the reader event handlers
            //reader.onprogress = handleReaderProgress; Commented out as we don't really need it
            reader.onloadend = handleReaderLoadEnd;

            // begin the read operation
            reader.readAsDataURL(file);
            this.UploadFile(file);
        }
        else {
            var message = "File type not valid. Only images are allowed.";
            $("#multiupload-alert-message").html(message);
            $("#multiupload-alert").show();
        }
    }
What I do here is to select the first (and only) element from the files array and check that it is of type image (remember that the dropzone attribute doesn’t work).
If the type matches, I can instantiate a new object of type FileReader().This interface provides such an asynchronous API.
The FileReader object has several events that can be catched (http://www.w3.org/TR/FileAPI/#event-handler-attributes-section)
event handler attribute event handler event type
onloadstart loadstart
onprogress progress
onabort abort
onerror error
onload load
onloadend loadend
For the purpose of this tutorial I will just handle the loadend event with the following line of code
reader.onloadend = handleReaderLoadEnd;

FileReader includes four options for reading a file, asynchronously:

  • FileReader.readAsBinaryString(Blob|File) – The result property will contain the file/blob’s data as a binary string. Every byte is represented by an integer in the range [0..255].
  • FileReader.readAsText(Blob|File, opt_encoding) – The result property will contain the file/blob’s data as a text string. By default the string is decoded as ‘UTF-8’. Using the optional encoding parameter it is possible to specify a different format.
  • FileReader.readAsDataURL(Blob|File) – The result property will contain the file/blob’s data encoded as a data URL.
  • FileReader.readAsArrayBuffer(Blob|File) – The result property will contain the file/blob’s data as an ArrayBuffer object.
I’m going to use readAsDataURL() so that I can assign the result of the read operation as the source of the image HTML element I use as a preview.
When the read operation is finished, the loadend event is raised and the following piece of code is executed
function handleReaderLoadEnd(evt) {
        var img = document.getElementById("preview");
        img.src = evt.target.result;
        var fileName = fileToUpload.name;
        if (fileName.length > 20) {
            fileName = fileName.substring(0, 20);
            fileName = fileName + "...";
        }
        $("#td-file-name").text(fileName);
        var size = fileToUpload.size / 1024;
        size = Math.round(size * Math.pow(10, 2)) / Math.pow(10, 2);
        $("#td-file-size").text(size + "Kb");
        $("#progressBarBox").show("fast");
    }
The key here is the result attribute of the FileReader interface.
Having used the readAsDataURL() method, the specification says that
“On getting, if the readAsDataURL read method is used, the result attribute MUST return a DOMString that is a Data URL [DataURL] encoding of the File or Blob‘s data.”
That’s why I’m able to do the following:
var img = document.getElementById("preview");
img.src = evt.target.result;

The rest of the code is to show basic file info to the user into a tabular format.

Part 2: Async file upload and View update

While the file is being read into memory, I can asynchrounously upload the droppped file to the server using the following code
// Uploads a file to the server
    function UploadFile(file) {
        var xhr = new XMLHttpRequest();
        xhr.upload.addEventListener("progress", function (evt) {
            if (evt.lengthComputable) {
                var percentageUploaded = parseInt(100 - (evt.loaded / evt.total * 100));
                $(".bar").css("width", percentageUploaded + "%");
            }
        }, false);

        // File uploaded
        xhr.addEventListener("load", function () {
            $(".bar").css("width", "100%");
        }, false);

        // file received/failed
        xhr.onreadystatechange = function (e) {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var link = "<a href=\"" + xhr.responseText + "\" target=\"_blank\">" + fileName + "</a>";
                    $("#td-file-name").html(link);
                }
            }
        };

        xhr.open("POST", "/Home/Upload", true);

        // Set appropriate headers
        xhr.setRequestHeader("Content-Type", "multipart/form-data");
        xhr.setRequestHeader("X-File-Name", file.fileName);
        xhr.setRequestHeader("X-File-Size", file.fileSize);
        xhr.setRequestHeader("X-File-Type", file.type);

        // Send the file
        xhr.send(file);
    }
This method is all about the XMLHttpRequest object, which to quote the W3.org spec “allows scripts to perform HTTP client functionality, such as submitting form data or loading data from a remote Web site.”
I use the XMLHttpRequest to send the file in async mode to the server, but before I actually start to send the file, I need to subscribe to a few events which will allow me to give the user feedback about the operation progress.
xhr.upload.addEventListener("progress", function (evt) {
            if (evt.lengthComputable) {
                var percentageUploaded = parseInt(100 - (evt.loaded / evt.total * 100));
                $(".bar").css("width", percentageUploaded + "%");
            }
}, false);
It is important not to confuse the XMLHttpRequest.upload progress event with the XMLHttpRequest progress event, as they serve different purposes.
Having subscribed to this event, I’m able to give feedback to the user about upload operation status.
// File uploaded
        xhr.addEventListener("load", function () {
            $(".bar").css("width", "100%");
        }, false);

        // file received/failed
        xhr.onreadystatechange = function (e) {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var link = "<a href=\"" + xhr.responseText + "\" target=\"_blank\">" + fileName + "</a>";
                    $("#td-file-name").html(link);
                }
            }
        };
Subscribing to the load event, I’m able to know when all the bytes have been sent.
onreadystatechange is an attribute that represents a function that must be invoked when readyState changes value.
The readyState attribute has 5 possible values
0 Uninitialized
The initial value.
1 Open
The open() method has been successfully called.
2 Sent
The UA successfully completed the request, but no data has yet been received.
3 Receiving
Immediately before receiving the message body (if any). All HTTP headers have been received.
4 Loaded
The data transfer has been completed.
I want to know when the file has been sent and processed by the server
if (xhr.readyState == 4) {
        if (xhr.status == 200) {
                   var link = "<a href=\"" + xhr.responseText + "\" target=\"_blank\">" + fileName + "</a>";
                    $("#td-file-name").html(link);
     }
}
status code 200 means Success, so I can safely read the server response using the responseText property.
I’m using this property to hold the virtual path to the uploaded image, so that the user will be able to retrieve the link to access the remote resource by clicking the link.Now that everything is setup, I can open a connection in async mode (the last parameter), specifying which Controller and which Action will handle the POST
xhr.open("POST", "/Home/Upload", true);
and finally I can start sending bytes
xhr.send(file);
Now all there is left to do is to write the server side code that will receive the file, save it locally and reply to the client with the virtual path to access the resource.
We just have a small problem to deal with: how do we retrieve the posted file when we use XMLHttpRequest.send() method?
HttpPostedFile in this situation will be null, as the content is not held into Request.Files object, but instead it is stored into Request.InputStream as a stream of bytes!
[HttpPost]
        public string Upload()
        {
            UploadedFile file = RetrieveFileFromRequest();
            string savePath = string.Empty;
            string virtualPath = SaveFile(file);

            return virtualPath;
        }

        private UploadedFile RetrieveFileFromRequest()
        {
            string filename = null;
            string fileType = null;
            byte[] fileContents = null;

            if (Request.Files.Count > 0)
            { //we are uploading the old way
                var file = Request.Files[0];
                fileContents = new byte[file.ContentLength];
                file.InputStream.Read(fileContents, 0, file.ContentLength);
                fileType = file.ContentType;
                filename = file.FileName;
            }
            else if (Request.ContentLength > 0)
            {
                // Using FileAPI the content is in Request.InputStream!!!!
                fileContents = new byte[Request.ContentLength];
                Request.InputStream.Read(fileContents, 0, Request.ContentLength);
                filename = Request.Headers["X-File-Name"];
                fileType = Request.Headers["X-File-Type"];
            }

            return new UploadedFile()
            {
                Filename = filename,
                ContentType = fileType,
                FileSize = fileContents != null ? fileContents.Length : 0,
                Contents = fileContents
            };
        }

I’m not displaying the code for method SaveFile()  and class UploadedFile, you can download the suource code should you need it.

And this is the result:

Result

Result

Conclusions

With this post I just scratched the surface, there’s much more about FileAPI and XMLHttpRequest: for example using FileAPI it is possible to pause and resume an upload as it supports file chunking.
A useful jQuery plugin that leverage the full potential of the technologies shown here is available at http://blueimp.github.com/jQuery-File-Upload/

11 thoughts on “Drag and Drop asynchronous file upload with HTML 5, File API, XMLHttpRequest and Asp.Net MVC 3

  1. I will immediately take hold of your rss feed as I
    can not to find your email subscription link or e-newsletter service.
    Do you’ve any? Please permit me realize in order that I may subscribe. Thanks.

  2. Hey there I am so excited I found your blog, I really found
    you by accident, while I was researching on Askjeeve for something
    else, Anyways I am here now and would just like to say many thanks for a fantastic post and a all round entertaining blog (I also love the theme/design), I don’t have time to look over
    it all at the minute but I have bookmarked it and also
    included your RSS feeds, so when I have time I will be
    back to read much more, Please do keep up the fantastic jo.

Leave a reply to jakescott Cancel reply