Dustin Horne

Unity / .NET / MVC / Web Development

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

 

 

Comments (14) -

I get errors here , it is unable find the jsonresult

var newImg = $.parseJSON($("#UploadTarget").contents().find("#jsonResult")[0].innerHTML);

Reply

do you have a full source code?

Reply

kpan -

You're right, I converted this from another project I'd been working on and in my haste to strip it down and simplify it I introduced a few bugs as follows:

1.  WrappedJsonResult should call "base.ExecuteContext(context)".  I did not execute it on the base class so it was creating a stackoverflow (infinite loop).  I also removed the redundant constructor.

2.  The file input element... for some reason I had <input type="imageFile" name="imageFile"... that should have been:
<input type="file" name="imageFile"...>

3.  On the UploadImage action, I specified the parameter type as:  HttpPostedFile imageFile.  That is incorrect... it should have been:
HttpPostedFileWrapper imageFile.

All of the issues have been corrected in the article and I put together the working (and tested) sample.  It's a Visual Studio 2010 project:

www.dustinhorne.com/files/AjaxFileUploadDemo.zip

Reply

this is awesome...i've been trying to achieve this for a couple of weeks now...works like a charm...i love you man!

Reply

Thanks. ;)  Glad I could help and glad you got it going!

Reply

Very nicely done.

Any particular reason why you are returning the content as JSON? Would it be possible to return a partial view using Razor?

Reply

There's no reason you couldn't return it any way you want.  Using JSON just stays consistent with AJAX calls, the difference in this case being that you have to wrap it.  One thing to note though, since it's being returned in an iFrame you would want to use a whole View and not a partial.  And it has to be done in an iFrame to facilitate the multi-part post with the image.  For any other AJAX call you could certainly return a partial view result but you'd still have to use javascript to stuff it on the page.

Reply

Thanks, I'll have a look at it. Reason I asked is that JSON has a limitation of about 100k bytes, which is a bit low in case of images:

      <webServices>
        <jsonSerialization maxJsonLength="102400"/> <!-- see msdn.microsoft.com/en-us/library/bb763183.aspx for maximum length of JSON data-->
      </webServices>

Reply

100k is a lot in this case.  One thing to note, the image data is not being returned in the JSON.  The JSON is only text and in this case includes the URL to the image but not the image itself.  So the image could be 10MB and it wouldn't matter.

Reply

There is a bug .When you upload image that over 4M,there is a bug.

Microsoft JScript runtime error: Access is denied.

Reply

Amir -

It's not a bug.  The default max allowed file size is 4MB.  Unfortunately there isn't a good way to return JSON back to the javascript in the event this error occurs since .NET redirects to an error page.  If you want to upload a file larger than 4MB you'll need to update your web.config file to allow larger file uploads.  I left out error trapping for simplicity but you can check the document in the iFrame and make sure it has the elements you expect.  

You could also create a custom error page and generate a hidden element that outputs information about errors as JSON so you could access it from your javascript if the error page was returned.

Reply

its useful....

Reply

Ramakrishnahna

thanks alot for your post... you saved my life from a long time search

Reply

You're very welcome. Smile  I'm glad you found it useful!

Reply

Add comment

biuquote
  • Comment
  • Preview
Loading