Archive

Archive for June, 2016

jQuery File Upload: IE9 and ASP.Net Web API File Uploading

June 2nd, 2016 No comments

    The blueimp jQuery File Upload plugin uses the XMLHttpRequest to pass the file data to a server (only IE10+). If browser doesn’t support Ajax file uploading, the plugin makes a workaround by dynamically creating IFrame and sending the data on behalf of it through the traditional form POST. The JavaScript responsible for the workaround resides in jquery.iframe-transport.js, which accompanies the basic jquery.fileupload.js. The following TypeScript code could be used to initialize the plugin and submit file data (the code is intentionally kept as simple as possible – no progress bars, validations and so on):

//...
private fileData: any = {};
//...
$(".filePicker").fileupload({
	autoUpload: false, // will be submitted once button is clicked
	method: "PUT",
	dataType: "json", 
	url: "api/someController/someMethod", // Web Api method to receive and process the file
	formData: () => { // additional parameters accompanying the file data
		return [{
			name: "bookName",
			value: $("#bookName").val()
		},
		{
			name: "bookGenre",
			value: $("#bookGenre").val()
		},
		{
			name: "bookAuthor",
			value: $("#bookAuthor").val()
		}];
	},
	add: (e: JQueryEventObject, data: any) => { // event handlers
		this.fileData = data;
		//...
	},
	done: (e: JQueryEventObject, data: any) => {
		//...
	},
	fail: (e: JQueryEventObject, data: any) => {		
		//...
	},
	always: (e: JQueryEventObject, data: any) => {
		//...
	}
});
//...
$("#loadBook").on('click', function () { // the button to initiate the file sending
	if (this.fileData && this.fileData.hasOwnProperty("process")) {

		// file data validation: size, extension, whatever else...
		
		this.fileData.process().done(() => { // file sending
			this.fileData.submit();
		});
	}
});

On the server side the following code receives and processes the file data:

using System;
using System.Text;
using System·Web;
using System·Web.Http;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Linq;
...
namespace DotNetFollower.Web.Controllers.Api
{
    [RoutePrefix("api/someController")]
	public class someController : ApiController
	{        
        public someController()
		{
            //...
		}
		
        [Route("someMethod")]
        [HttpPut]
        [HttpPost] // this attribute allows processing traditional form POST
        public ServiceResult<BookOutput> someMethod()
        {
            try
            { 
			    //...
			    var request = HttpContext.Current.Request;
			    var files   = request.Files;

			    if (files.Count == 0) 
				    throw new Exception("Couldn't find a book to load!");

			    // read accompanying parameters
			    var bookName   = request.Form.Get("bookName");
			    var bookGenre  = request.Form.Get("bookGenre");
			    var bookAuthor = request.Form.Get("bookAuthor");
			
			    var file = new HttpPostedFileWrapper(files[0]);
			
			    // parsing file.InputStream ...
			
			    // processing the parsed file data ...
			
			    file.InputStream.Close();
			    //...
			
			    return new ServiceResult<BookOutput>() 
				    { 
					    Data = new BookOutput() 
						    { 
							    Name   = bookName, 
							    Genre  = bookGenre, 
							    Author = bookAuthor 
						    } 
				    };
            }
            catch(Exception ex)
            {
                return new ServiceResult<BookOutput>(ex);
            }
        }
	}
}

// Where ServiceResult and BookOutput are defined as follows

public class ServiceResult<T>
{
	public bool   Success      { get; set; }
	public string ErrorMessage { get; set; }	
	public T      Data         { get; set; }

	public ServiceResult()
	{
		Success = true;
	}

	public ServiceResult(string errorMessage)
	{   
		ErrorMessage = errorMessage;
	}

	public ServiceResult(Exception exception)
	{
		ErrorMessage = exception.Message;	
	}
}

public class BookOutput
{
	public string Name   { get; set; }
	public string Genre  { get; set; }
	public string Author { get; set; }
}

Unfortunately, the code doesn’t works as expected in Internet Explorer 9 (thankfully, the lower versions are not supposed to be supported by the project, so I don’t care about them). If IE9 sends an Ajax request to the Web Api method, it interprets the JSON response correctly. However, when uploading a file, the jQuery File Upload plugin sends traditional non-Ajax form POST. So, having received the JSON result, IE9 prompts for a JSON file download.

Download Json File Prompt

To bypass such IE9 behaviour the server response should contain the content-type header “text/html” rather than the “application/json” returned by the Web Api method by default. To avoid writing some IE9 specific logic on both server and client sides, I’ve introduced the following Web Api method-adapter:

[Route("someMethodAdapted")]
[HttpPut]
[HttpPost] // this attribute allows processing traditional form POST
public HttpResponseMessage someMethodAdapted()
{
	var res = someMethod(); // call the original method

	const string JsonContentType = "application/json";
	const string HtmlContentType = "text/html";

	var response = Request.CreateResponse(HttpStatusCode.OK); // return 200 OK
	var context  = HttpContext.Current;
	response.Content = new StringContent(ToJson(res), // serialize result object into JSON
	  Encoding.UTF8, 
	  // check if the JSON content-type is accepted 
	  // (it's not accepted in case of form POST coming from IE9)
	  context.Request.AcceptTypes.Contains(JsonContentType, StringComparer.OrdinalIgnoreCase) ?
	     JsonContentType : HtmlContentType); // return suitable content-type

	return response;
}

// the ToJson method is defined as follows

private static string ToJson<T>(T obj) where T : class
{
	if (obj == null)
		return string.Empty;

	DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
	using (MemoryStream stream = new MemoryStream())
	{
		serializer.WriteObject(stream, obj);
		return Encoding.Default.GetString(stream.ToArray());
	}
}

The someMethodAdapted is supposed to be used instead of someMethod everywhere on the client side. So, repoint the url to the method-adapter

...
$(".filePicker").fileupload({
	...
	// Web Api method-adapter to receive and process the file
	url: "api/someController/someMethodAdapted",
	...
});
...

The someMethodAdapted makes a content-type trick and perfectly serves IE9 and higher. The use of HttpResponseMessage gives a control over the response headers. The HttpContext.Current.Request.AcceptTypes is a list of client-supported content types (aka MIME types). If the “application/json” is not in the list, the “text/html” is the right choice. Below are the AcceptTypes of Ajax and non-Ajax requests made by IE9:

// IE9 Ajax request to a Web Api method (true for higher browser versions too)
HttpContext.Current.Request.AcceptTypes	{string[3]}	string[]
[0]	"application/json"	string
[1]	"text/javascript"	string
[2]	"*/*; q=0.01"	string

// IE9 non-Ajax form POST
HttpContext.Current.Request.AcceptTypes	{string[3]}	string[]
[0]	"text/html"	string
[1]	"application/xhtml+xml"	string
[2]	"*/*"	string