Handling uploads with MVC4, JQuery, Plupload and CKEditor

I spent a considerable amount of time trying to get MVC to work nicely with various file uploaders while I was writing RustyShark, and I have to admit it's been pretty frustrating at times. On the plus side, it's taught me a lot about how Microsoft MVC works behind the scenes, and allowed me to play with quite a few different uploading tools that're out there.

My upload page on RustyShark has quite a complex model - each review is related to multiple Formats (PS3, 360, DVD, Blu Ray etc), each review / format combination can have multiple images associated with it, and each image has additional meta data attached, such as a caption and an upload date. The upload form is created using a combination of Editor Templates, JQuery and AJAX. I will be stripping down any examples to their bare bones to make things a little clearer.

This article assumes you are familiar with MVC fundamentals such as creating controllers, views and models within Visual Studio 2010 or later, you understand the Razor view engine syntax, you know how to use MVC Editor Templates, and you are fluent with the .Net Framework v4.0 or later. All code is provided as an example only, and is untested.

Input type="file"


To start, let's look at a standard <input type="file" /> upload. First, set up a controller:

public class MyController : Controller {
    public ActionResult UploadImage() {
        return View();
    }
}

Then create a view for the controller called "UploadImage.cshtml" that will contain the form mark-up.

When it comes to building your form, first and foremost (and something quite a few people forget) is the encoding type of the form submitting the file data:

@using(Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" })) {
    <input type="file" id="testingupload" name="testingupload"/>
    <input type="submit" value="Upload image"/>
}

The vast majority of the time in MVC you'll be using Html.BeginForm() without any parameters - remember, all a HTML helper does is output HTML, and most helpers have the "htmlAttributes" parameter to catch any attributes that aren't catered for by defined arguments. Here, we've added the "enctype" <form> attribute and set it so we can upload binary data when the form is submitted.

By default, when a form action isn't set, HTML forms POST to the current page. You don't need to set the "action" or "controller" parameters unless you're posting to a different location (which I only really use in shared partials or special AJAX forms anyway), hence why the first two parameters in my example are "null".

Note that the "id" attribute of the file input is not required for this example. However, it is recommended that you give all form elements an ID, to identify them as a unique element in the HTML DOM. The "name" attribute is the important thing here, though this does not need to be unique. You can give multiple form elements the same name so they're posted to the server as a collection, but you already knew that, right? :) More on that later.

You now have a page that should compile, but you'll get an error when you click the submit button as there's no code to handle the postback. Add the following to your controller:

[HttpPost]
public ActionResult UploadImage(HttpPostedFileBase testingupload) {
    return View();
}

As with all form posting in MVC, the HttpPost action filter restricts the action so that it only accepts the POST verb from your browser. The only other notable thing here is the single parameter - HttpPostedFileBase, and its name, "testingupload", which should always match the file input's "name" attribute. This (along with the parameter's type) allows MVC to automagically map form values to method parameters. Neat!

Now, when you select an image to upload, and click the submit button, the "testingupload" parameter should be populated with all the details you need for the file you've selected. From there you can do what you want - there's a "SaveAs" method on the HttpPostedFileBase object that allows you to easily save the file to disk, though you'll usually want to do a little bit more than just that, like resize an image or notify someone the upload has completed.

That's a simple file upload example, and as you can see there's nothing to it; but only really much use if you plan to upload a single file.

There's one last thing I need to mention before I move on - you can (and should, in this developer's opinion) implement your file input in a view model as you would any other form property, which makes the whole process a lot cleaner and more robust. Below is a sample model:

public class MyModel {
    public int EntityID { get; set; }
    [Display(Name = "Select the image you would like to upload.")]
    [Required(AllowEmptyStrings = false, ErrorMessage = "Please select the image to upload.")]
    public HttpPostedFileBase ImageToUpload { get; set; }
}

And the corresponding view:

@model MyModel
@using(Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" })) {
    @Html.HiddenFor(m => m.EntityID)
    <div>
        <fieldset>
            <legend>Upload my image</legend>
            <div class="formField">
                <div class="label">
                    @Html.LabelFor(m => m.ImageToUpload)
                </div>
                <div class="input">
                    @Html.FileUploadFor(m => m.ImageToUpload)
                </div>
                @Html.ValidationMessageFor(m => m.ImageToUpload)
            </div>
            <input type="submit" value="Upload!"/>
        </fieldset>
    </div>
}

MVC does not provide a helper for file upload inputs. The @Html.FileUploadFor() is a custom helper I've written that allows you to bind to your model, and it looks like this:

public static MvcHtmlString FileUploadFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool multiple = false) {
    object props;
    if(multiple) {
        props = new {
            type = "file",
            multiple = "multiple",
            @class = "file"
        };
    } else {
        props = new {
            type = "file",
            @class = "file"
        };
    }
    return helper.TextBoxFor(expression, props);
}

Discerning readers will have noticed that I've implemented the "multiple" attribute too... more on that later.

Unfortunately, without using some crazy reflection code, I don't think I can avoid using the if-else statement (please, please, please tell me if I'm wrong!). I usually prefer ternary operators for this kind of thing, but it's not possible with anonymous objects.

So, after you start using a strongly-typed view model, your POST controller method would look like this:

[HttpPost]
public ActionResult UploadImage(MyModel model) {
    var image = model.ImageToUpload;
    // TODO: Do something with image
    return RedirectToAction("UploadImage");
}

The above described method forms a solid basis for understanding how any of the other file upload tools work, so commit it to memory and get ready to do some serious configuration...


JQuery Uploadify


I used JQuery uploadify for the first release of RustyShark, but very quickly realised it couldn't do everything I wanted, and sometimes just stoped dead when uploading a lot of files or using multiple uploaders on one page. I also had a trouble manipulating the generated HTML elements and attaching to various events. During my development phase, a new version was released which - whilst much better - completely broke everything I'd already done; for that reason, I'm going to outline a number of gotchas, but not actually post any plugin code (I have since stripped it out, and I don't have the energy to rewrite it here).

My Uploadify implementation was sat in a control panel, behind .Net Forms Authentication. This created a security issue, which took me a good while to pin down; I was receiving a 403 when posting the image to the controller, which made little sense as I was logged in and all other form posts were working fine. It turned out that it was the flash object that Uploadify implements that didn't have the access, because the ASPNET Session cookie wasn't passed through on the form post. To rectify it, I needed to include the ASPNET session / cookie information in the form post manually, and write some code to create the ASPNET session cookies at the Application_BeginRequest() stage. In short, it was a right ball-ache.

PLUpload


After my catastrophe with Uploadify, I tried out a few more JQuery uploaders (and even experimented with writing my own), finally settling on PLUpload. Whilst it has it's own little issues, I actually found it to be a lot more robust and reliable than any of the other plugins I tried. Again, I spent minimal time on customising the look and feel, but I'm confident I could integrate it in to my site completely with a few hours of styling.

PLUpload was particularly appealing because it renders different implementations of itself based upon the user's browser capabilities, and supports multi-file uploads out of the box. Although I haven't tested it in a great amount of detail, you can implement browserplus, google gears, html5 or js - which means it's compatible with a wide range of browsers. It also offers an easy way to delete any accidentally uploaded items on the same screen, and lets you use AJAX to perform all operations.

The implementation I went with is way to complex to copy + paste here, so yet again I will break it down to the fundamentals to make it's use a bit clearer. My actual implementation uses both the input type="file" example above, plus it iterates though a custom object (IEnumerable<ImageModel>), rendering the PLUpload control for each item. Below, I'll just show you a single PLUpload control, which is all handled with AJAX calls to one of my MVC controllers.

Model
namespace Upload {
 public class ImageModel {
  public int UniqueId { get; set; }

  [Display(Name = "Select any images related to this format.")]
  public object PLUploadPlaceholder { get; set; }
 }
}

The "PLUploadPlaceholder" property above exists only to allow us to use MVC helpers to render the HTML, and use the DataAnnotation attributes to provide metadata to the view. My model is considerably more complex than this, so it made total sense to put this little bit of extra effort in, though you could easily get away with omitting the model entirely and rendering the PLUpload HTML manually. The UniqueId property is used to distinguish between each PLUpload control. Mine comes from a database.

 public class UploadingImageModel {
  public int UniqueId { get; set; }
  public HttpPostedFileBase file { get; set; }
  public string name { get; set; }
 }

The above class is used for the upload request. "file" and "name" are posted automatically from PLUpload, whereas "UniqueID" is set in the JavaScript during each upload.

View
@model ImageModel
<div class="plUploadContainer">
 @Html.Hidden("AUTHID", Request.Cookies[FormsAuthentication.FormsCookieName] != null ? Request.Cookies[FormsAuthentication.FormsCookieName].Value : string.Empty)
 @Html.Hidden("ASPSESSID", Session.SessionID)
 @Html.HiddenFor(m => m.UniqueId)
 <fieldset>
  <legend>Upload Your Files</legend> 
  <div class="formField">
   <div class="label">
    @Html.LabelFor(m => m.PLUploadPlaceholder)
   </div>
   <div class="input">
    <div class="file">
     <div id="@Html.ClientIDFor(m => m.PLUploadPlaceholder)" title="@Html.ClientNameFor(m => m.PLUploadPlaceholder)"></div>
    </div>
   </div>
  </div>
 </fieldset>
</div>

The above View is actually a modified version of the Editor Template I use within my view model structure (I use @Html.EditorFor() to render each item of the IEnumerable<ImageModel> in my parent view model), but it serves perfectly well as the view if you only want one PLUpload control.

Secondly, notice that the UniqueId property is being written out to a hidden form control. We will use JQuery traversal to retrieve that Id, and send it as a POST parameter to the server.

Thirdly, the other two hidden fields AUTHID and ASPSESSID are used to get around a flash authentication issue I was running into. In hindsight, I really should have created some HTML helpers for these fields, as they expose too much logic to the view. Flash based uploaders do not forward ASP.Net session information to the URLs they send their requests to (the requests are anonymous and probably create a new session), so when you're doing this behind Forms Authentication, you have to mimic this behaviour yourself. To get this working, you will also need to put the following code into your Global.asax file:

protected void Application_BeginRequest(object sender, EventArgs e) {
    var ctx = HttpContext.Current;

    try {
        const string sessionParamName = "ASPSESSID";
        const string sessionCookieName = "ASP.NET_SessionId";

        if(ctx.Request.Form[sessionParamName] != null) {
            UpdateCookie(sessionCookieName, ctx.Request.Form[sessionParamName]);
        } else if(ctx.Request.QueryString[sessionParamName] != null) {
            UpdateCookie(sessionCookieName, ctx.Request.QueryString[sessionParamName]);
        }
    } catch(Exception ex) {
        // Do something with the exception
    }

    try {
        const string authParamName = "AUTHID";
        var authCookieName = FormsAuthentication.FormsCookieName;

        if(ctx.Request.Form[authParamName] != null) {
            UpdateCookie(authCookieName, ctx.Request.Form[authParamName]);
        } else if(ctx.Request.QueryString[authParamName] != null) {
            UpdateCookie(authCookieName, ctx.Request.QueryString[authParamName]);
        }
    } catch(Exception ex) {
        // Do something with the excetion
    }
}

private static void UpdateCookie(string name, string value) {
    var cookie = HttpContext.Current.Request.Cookies.Get(name) ?? new HttpCookie(name);
    cookie.Value = value;
    HttpContext.Current.Request.Cookies.Set(cookie);
}

The last thing to mention in the view is the two helper methods, @Html.ClientIDFor() and @Html.ClientNameFor(). The code for these is below, and basically allow me to use property meta data for rendering custom controls, debugging or simply displaying the data to the user. Here, I'm using it to render a <div> with a unique id that we can replace with a PLUpload plugin.

  public static MvcHtmlString ClientIDFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) {
   return MvcHtmlString.Create(
    htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(
     ExpressionHelper.GetExpressionText(expression)));
  }

  public static MvcHtmlString ClientNameFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel, TProperty>> expression) {
   return MvcHtmlString.Create(
    htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(
     ExpressionHelper.GetExpressionText(expression)));
  }

Controller
public class UploadController: Controller {
  [HttpPost]
  public JsonResult UploadImage(UploadingImageModel model) {
   if (model.file != null) {
    var uploadedImageResult = MyService.UploadMyImage(model);
    if (!relatedImageResult.Success) {
     // Throw exception so javascript can handle it
     throw new Exception(relatedImageResult.ErrorMessage);
    }
    // Return object as Json string
    return Json(uploadedImageResult);
   }
   throw new ArgumentException("'file' is null");
  }

  [HttpPost]
  public JsonResult DeleteImage(int imageId) {
   // Try to delete the image
   var deletedImageResult = MyService.DeleteMyImage(imageId);
   if (!deletedImageResult.Success) {
    // Throw exception so javascript can handle it
    throw new Exception(deletedImageResult.ErrorMessage);
   }
   return Json(deletedImageResult);
  }
}

These two action methods are called via AJAX to perform an individual upload and delete of each image. MyService is a business component that deals with the complex logic for each method. However, the details of this are out of scope in this post.

JQuery

The JQuery required to get PLUpload working isn't very complicated (assuming you've installed it correctly and referenced the correct files), so I'll just annotate the code itself below:

$(document).ready(function () {
    // Used to retrieve the correct uploader for the current ajax request.
    function getUploader(file) {
        return $("#" + file.id).parents('div[id$="PLUploadPlaceholder"]');
    }

    // Get all uploaders on the page
    var uploaders = $('div[id$="PLUploadPlaceholder"]');

    // Apply PLUpload to each of the above DIVs
    uploaders.plupload({
        // Setting details available here: http://www.plupload.com/documentation.php
        runtimes: 'html5,flash,browserplus',
        flash_swf_url: '/res/img/l/u/plupload.flash.swf',
        url: '/Upload/UploadImage/', // Determines the URL to POST each upload to.
        max_file_size: '10mb',
        unique_names: true, // Generates client side unique names before the files are uploaded to the server
        multipart: true,
        // Specify what files to browse for in the client side browse dialogue
        filters: [{
            title: "Image files",
            extensions: "jpg,jpeg,gif,png"
        }],
        preinit: {
            // The preinit.UploadFile event is called just before a file is uploaded
            UploadFile: function (up, file) {
                // Use JQuery traversal to retrieve the unique ID (and any other information you might need)
                var uniqueId = getUploader(file)
                 .closest('.plUploadContainer')
                 .children('input[type="hidden"][name$="UniqueId"]')
                 .first()
                 .val();

                // Set up the additional parameters to be sent with the upload request.
                up.settings.multipart_params = {
                    ASPSESSID: $("#ASPSESSID").val(),    // Ignored by the action method, used internally by ASP.Net
                    AUTHID: $("#AUTHID").val(),            // Ignored by the action method, used internally by ASP.Net
                    UniqueId: uniqueId                    // Used by the action method, shown on the UploadingImageModel above.
                };
            }
        },
        init: {
            // The init.FilesRemoved event is called when the user requests the deletion of a file from the PLUpload UI.
            FilesRemoved: function (up, files) {
                // Loop through each file selected for removal and make an ajax call to the server
                $.each(files, function (i, file) {
                    $.ajax({
                        url: '/Upload/DeleteImage',
                        async: true,
                        type: "POST",
                        data: {
                            imageId: file.ImageId
                        }
                    });
                });
            },
            // The init.FileUploaded event is called after a file has been uploaded to the server.
            FileUploaded: function (up, file, info) {
                // Upload Success. Get the response as json
                var json = $.parseJSON(info.response);
                if (json) {
                    // Add the newly generated ImageId to the PLUpload file (so we can use it for deleting images)
                    $.extend(file, { ImageId: json.ImageId });
                    return;
                }

            }
        }
    });
});

And that's it. Obviously I haven't provided a 100% complete solution here, but you get the general idea of how it works. I'm sure there are things you can do to clean things up a little bit, but hopefully this will give you a good head start!

CKEditor Custom Uploader


CKEditor is a fairly bulky, but highly configurable and feature rich JavaScript based HTML editor. As is the case with an uncanny number of JS plugins out there, the documentation is not particularly intuitive, though there is a lot of it. Luckily, to simplify(!) matters, a JQuery plugin is available.

By default it uses an integrated single-image upload form, which can be customised if you wish. My requirements are simple and I don't care much for layout / looks at the moment, as long as it works; I wanted to upload an image, resize it and place it inside my review / news articles. Suffice to say, I had to bust out Fiddler to figure out what was actually going on in the background in order to get it working with MVC.

As with the input type="file" example above, CKEditor posts form values directly to the server, along with a few other details that may or may not be useful to you. You can safely use the following MVC action to pick up the CKEditor upload post:

public ActionResult UploadImage(HttpPostedFileBase upload, string CKEditorFuncNum, string CKEditor, string langCode) {
    var output = string.Concat("<html><body><script>window.parent.CKEDITOR.tools.callFunction(",
        CKEditorFuncNum, ", \"{0}\", \"{1}\");</script></body></html>");
    // Do something with the image

    // Return the content expected by CKEditor
    var webPath = "/images/path-to-my-new-image.jpg";
    var error = "Any custom errors associated with uploading";
    return Content(string.Format(output, webPath, error));
}

You could, in theory turn the action parameters into its own model, but I didn't bother myself - mainly due to the fact that no other form values are sent to the server, they're case sensitive, and I'm a stickler for consistency. If I implemented it, the model would look like this (note the varying case of members):

public class CKUploadModel {
    public HttpPostedFileBase upload { get; set; }
    public string CKEditorFuncNum { get; set; }
    public string CKEditor { get; set; }
    public string langCode { get; set; }
}

Once installed, you can implement the upload features of CKEditor via JQuery with as little code as this:

$("#MyEditor").ckeditor({
    filebrowserUploadUrl: '/MyController/UploadImage/'
});

Which is nice and neat. Installation isn't all that hard - as usual, it means copying some files to your web site and including them on the page. Read more about it here.

The above example isn't without its issues. One interesting thing to note is that I had a few problems when I wanted to send additional parameters with the upload POST. CKEditor doesn't have anything built in to it extend the POST parameters, but you can - with a lot of faffing - append a querystring to the filebrowserUploadUrl, and send additional information through to the action method. This, however, requires you to destroy and reload the editor each time you want to amend the querystring, which in my case, meant writing a few functions that I called whenever the relevant control on the parent form changed:

$("#SomeFormControl").change(function () {
    setFileBrowserUrl();
});
function setFileBrowserUrl() {
    // Remove editor instance
    $("#Content").ckeditorGet().destroy();
    // Recreate editor instance (needed to reset the file browser url)
    createEditor();
}
function createEditor() {
    $('#Content').ckeditor({
        filebrowserUploadUrl: '/MyController/UploadImage/?MyCustomID=' + $("#SomeFormControl").val()
    });
}

This then allows me to modify my UploadImage action method as such:

public ActionResult UploadImage(HttpPostedFileBase upload, string CKEditorFuncNum, string CKEditor, string langCode, int MyCustomID) {
    var output = string.Concat("<html><body><script>window.parent.CKEDITOR.tools.callFunction(",
        CKEditorFuncNum, ", \"{0}\", \"{1}\");</script></body></html>");
    // Do something with the image and MyCustomID

    // Return the content expected by CKEditor
    var webPath = "/images/path-to-my-new-image.jpg";
    var error = "Any custom errors associated with uploading";
    return Content(string.Format(output, webPath, error));
}

It's not pretty, and it's not the most efficient piece of code I've ever written, but as far as I can tell, it's the only way to change settings within CKEditor on the fly. When looking through the CKEditor code, I noticed that the properties seem to be initialized lazily, meaning that they're cached for the duration of the object's lifetime, and any values that are set against the properties are ignored unless it's reloaded.

At the moment, CKEditor is my rich text editor of choice. I'm yet to find another (free) editor that has all the features I want.

Popular posts from this blog

Getting Unity 3D working with Git on Windows

Generating a self-signed SSL certificate for my QNAP NAS