Dustin Horne

Developing for fun...

AJAX File Uploads with jQuery and MVC 3

Recently I was working on a project that required upload of images.  I wanted to upload the images AJAX style without refreshing the page and thought jQuery would be a good fit.  Using jQuery to post standard forms is extremely simple, but when posting multi-part forms for uploading files it's not so intuitive.  This is due to browser security restrictions and sandboxing.  Today I'm going to show you how to fake an asynchronous upload without reloading the page and get a result back from the server.

How It Works

In order to make the AJAX style file uploads work, we need to to post to an iFrame.  We're going to position an iFrame off the page to hide it and then post to it.  We'll return a result to the page and retrieve it with our javascript.  It's important to note that due to security limitations, the page being posted to must be on the same domain as the page you're posting from.  Also, I won't be demonstrating any progress indicators but in production I would highly recommend you disable the upload from and use a progress indicator to let your users know the file is uploading.  Also, this solution will be using a single hidden iFrame, but for multiple simultaneous uploads you could dynamically generate the iFrames and post to them.

A Couple of Problems

Before we get into details I want to point out a couple of issues that I ran into.  First of all, if you're returning a standard JsonResult from your MVC Controller, the browser will prompt you to download the file.  To avoid this issue, we're going to inherit from the base JsonResult class and create our own that will wrap the JsonResult in a named textarea element and set the return content type to text/html.

The second issue is in attaching the onload event to the iFrame.  I literally spent hours trying to use jQuery to properly attach an event to the iFrame, using different permutations of both .load() and .ready().  For some reason these events were always attached instead to the parent page and only ever fired when the parent page was first ready.  Because of this and a couple other small issues that are likely due to my limited jQuery knowledge, I used a mix of jQuery and straight up javascript in my application.  I also manually set the OnLoad event of the iFrame.  The iFrame's onload event will fire the first time it's loaded, so I also created a boolean variable to track whether or not it's the first load.  So let's get to the code.

WrappedJsonResult

using System.Web.Mvc;

namespace jQueryUploadSample
{

	public class WrappedJsonResult : JsonResult
	{
		public override void ExecuteResult(ControllerContext context)
		{
			context.HttpContext.Response.Write("<html><body><textarea id=\"jsonResult\" name=\"jsonResult\">");
			base.ExecuteResult(context);
			context.HttpContext.Response.Write("</textarea></body></html>");
			context.HttpContext.Response.ContentType = "text/html";
		}
	}
}

As you can see, we're using a standard JsonResult and we're wrapping the output in an html document and a textarea.  This will allow us to use jQuery to access the value of that textarea from our parent page.  Next we'll work on our controller method that accepts the upload.

UploadImage Controller Method

To keep things simple we're just going to do everything in our controller method.  We're also not going to do any validation, we're going to assume that every file uploaded is a jpg and we're going to return the path to the image as well as a status message.  To demonstrate what to do in the event of an error, we will also return some status information in our JSON result.  The beauty of dynamics in .NET and JSON allows us to do a lot of great stuff.  For now, let's go ahead and create a controller method. 

 This method will accept an HttpPostedFile called "imagefile", save the file and return information back to our script.  It will also assume that your images will be saved to a /Content/UserImages folder. We're going to generate a GUID to use as the image file name.  Add the following method to a controller (preferrably one with the [Authorize] attribute set):

		[HttpPost]
		public WrappedJsonResult UploadImage(HttpPostedFileWrapper imageFile)
		{

			if (imageFile == null || imageFile.ContentLength == 0)
			{
				return new WrappedJsonResult
								{
									Data = new
									{
										IsValid = false,
										Message = "No file was uploaded.",
										ImagePath = string.Empty
									}
								};
			}
			
			var fileName = String.Format("{0}.jpg", Guid.NewGuid().ToString());
			var imagePath = Path.Combine(Server.MapPath(Url.Content("~/Content/UserImages")), fileName);

			imageFile.SaveAs(imagePath);

			return new WrappedJsonResult
								{
									Data = new
									{
										IsValid = true,
										Message = string.Empty,
										ImagePath = Url.Content(String.Format("~/Content/UserImages/{0}", fileName))
									}
								};
		}

Examining the code above you can see that we're first checking to see if an image was uploaded.  If not, we're returning a WrappedJsonResult reporting the error to the calling script.  If a file was there we generate a file name, save it to the server, and return the information back to the script.  I should note that in the event of an error, it would not be necessary to return ImagePath and in the event of a succesful save you would not need to return Message.  I've included it in both return values for completeness and so the javascript can assume that it's always there.

Setting Up the HTML

The next thing we need to do is setup the HTML for our form.

@using (Html.BeginForm("UploadImage", "YourController", FormMethod.Post, 
	new { enctype = "multipart/form-data", id="ImgForm", 
		name="ImgForm", target="UploadTarget" }))
{
	<h3>Upload Image</h3>
	<input type="file" name="imageFile" />

	<input type="button" value="Save Image" onclick="UploadImage()" />
}
<iframe id="UploadTarget" name="UploadTarget" onload="UploadImage_Complete();" style="position: absolute; left: -999em; top: -999em;"></iframe>
<div id="Images"></div>

The name of your file input "imageFile" must match the parameter name of your UploadImage method.  Make sure you replace "YourController" with the name of the controller UploadImage is in.  You'll also notice that I've positioned the iFrame off of the screen rather than simply changing it's display property via CSS.  Some browser versions (I believe older versions of Firefox and Safari) do not always play nice with hidden iFrames so I've just positioned it off the page instead.  We've also added an "Images" div that we'll use to shovel the images in after they've been uploaded.

Now all that's left to do is setup our Javascript.  We'll need an UploadImage() method that will handle uploading the image and an UploadImage_Complete method that will fire when the iFrame has finished loading.

Setting Up the Javascript

The first thing we need to do is include a version of jQuery.  Since I'm writing an MVC application, I've chosen to use Microsoft's CDN and I'm using jQuery 1.7.  Add the following line to the HEAD section of your page:

<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.min.js" type="text/javascript"></script>

Now we can start creating our javascript to handle our form posts.  The UploadImage() method isn't absolutely necessary since all our example does is post the form, however if you wanted to display a busy indicator it would be a good place to do it.  Here we're just going to create a global variable that tells whether or not we're on the first load of the iFrame and the function to upload the image:

var isFirstLoad = true;

function UploadImage() 
{
	$("#ImgForm").submit();	
}

Now the form is setup to post to our iFrame.  All we have to do is retrieve the result of the post and display the new image.  Here is the code to retrieve the result and handle the completion of the image upload:

function UploadImage_Complete() 
{
	//Check to see if this is the first load of the iFrame
	if (isFirstLoad == true) 
	{
		isFirstLoad = false;
		return;
	}

	//Reset the image form so the file won't get uploaded again
	document.getElementById("ImgForm").reset();

	//Grab the content of the textarea we named jsonResult .  This shold be loaded into 
	//the hidden iFrame.
	var newImg = $.parseJSON($("#UploadTarget").contents().find("#jsonResult")[0].innerHTML);

	//If there was an error, display it to the user
	if (newImg.IsValid == false) 
	{
		alert(newImg.Message);
		return;
	}

	//Create a new image and insert it into the Images div.  Just to be fancy, 
	//we're going to use a "FadeIn" effect from jQuery
	var imgDiv = document.getElementById("Images");
	var img = new Image();
	img.src = newImg.ImagePath;

	//Hide the image before adding to the DOM
	$(img).hide();
	imgDiv.appendChild(img);
	//Now fade the image in
	$(img).fadeIn(500, null);
}

Conclusion

And that's it.  Technically there wasn't a lot of jQuery work done, however we did demonstrate some of it and we can upload a file to our server in an AJAXy fashion. 

Downloads

You can download the Visual Studio 2010 Solution here: 
AjaxFileUploadDemo.zip