As I’ve mentioned before, I really, really hate the way most people seem to be creating reusable UI “controls” with ASP.NET MVC. I do not like emitting JavaScript, HTML, etc. from within C# code. It’s cumbersome to create, difficult to really test, and just a real PITA in general.
Based on feedback I received from Rob after my attempts at creating a helper for jqGrid, I decided to take a completely different approach when it was time to wrap another jQuery plug-in: Uploadify. My goal was to minimize the amount of tag-soup embedded in my C# code while still maintaining the ease-of-use of the jqGrid helper, which required only a single HtmlHelper call to go from nothing to full grid.
Well, one painful afternoon later, I think I’ve arrived at something that makes some sense. First, I couldn’t completely eliminate the tag soup, but I did minimize it (I think) while still keeping the thing extremely simple to use and (hopefully) maintain. Let’s start with how you would use it:
<asp:Content ContentPlaceHolderID="HeadContent" runat="server"> <%=Html.Uploadify("fileInput", new UploadifyOptions { UploadUrl = Html.BuildUrlFromExpression<SandboxController>(c => c.HandleUpload(null)), FileExtensions = "*.xls;*.xlsx", FileDescription = "Excel Files", AuthenticationToken = Request.Cookies[FormsAuthentication.FormsCookieName] == null ? string.Empty : Request.Cookies[FormsAuthentication.FormsCookieName].Value, ErrorFunction = "onError", CompleteFunction = "onComplete" }) %> <script type="text/javascript"> function onError() { alert('Something went wrong.'); } function onComplete() { alert('File saved!'); } </script> </asp:Content>
The first parameter is the name of the input control to convert to an uploadify control, the second contains all the optional settings you can customize. I prefer to use an options class like this rather than provide 50,000 overloads. By using a dedicated options class, I can add new settings without breaking existing code or having to create new overloads. The options should be fairly self explanatory, but here they are:
/// <summary> /// Defines all options for <see cref="HtmlHelperExtensions.Uploadify"/>. /// </summary> public class UploadifyOptions { #region Public Properties /// <summary> /// The URL to the action that will process uploaded files. /// </summary> public string UploadUrl { get; set; } /// <summary> /// The file extensions to accept. /// </summary> public string FileExtensions { get; set; } /// <summary> /// Description corresponding to <see cref="FileExtensions"/>. /// </summary> public string FileDescription { get; set; } /// <summary> /// The ASP.NET forms authentication token. /// </summary> /// <example> /// You can get this in a view using: /// <code> /// Request.Cookies[FormsAuthentication.FormsCookieName].Value /// </code> /// You should check for the existence of the cookie before accessing /// its value. /// </example> public string AuthenticationToken { get; set; } /// <summary> /// The name of a JavaScript function to call if an error occurs /// during the upload. /// </summary> public string ErrorFunction { get; set; } /// <summary> /// The name of a JavaScript function to call when an upload /// completes successfully. /// </summary> public string CompleteFunction { get; set; } #endregion }
Next, we have the actual HtmlHelper extension method:
/// <summary> /// Renders JavaScript to turn the specified file input control into an /// Uploadify upload control. /// </summary> /// <param name="helper"></param> /// <param name="name"></param> /// <param name="options"></param> /// <returns></returns> public static string Uploadify(this HtmlHelper helper, string name, UploadifyOptions options) { string scriptPath = helper.ResolveUrl("~/Content/jqueryPlugins/uploadify/"); StringBuilder sb = new StringBuilder(); //Include the JS file. sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.js")); sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.init.js")); //Dump the script to initialze Uploadify sb.AppendLine("<script type=\"text/javascript\">"); sb.AppendLine("$(document).ready(function() {"); sb.AppendFormat("initUploadify($('#{0}'),'{1}','{2}','{3}','{4}','{5}',{6},{7});", name, options.UploadUrl, scriptPath, options.FileExtensions, options.FileDescription, options.AuthenticationToken, options.ErrorFunction ?? "null", options.CompleteFunction ?? "null"); sb.AppendLine(); sb.AppendLine("});"); sb.AppendLine("</script"); return sb.ToString(); }
The helper uses a StringBuilder (yeah, I hate them, and I’m open to suggestions) to include two JavaScript files. The first is the standard uploadify script, but the second is something custom, which I’ll get to in just a second. Finally, the helper outputs a call to initUploadify inside of the page load event, passing in all the options that were specified.
And that brings us to that second JavaScript include:
//This is used in conjunction with the HtmlHelper.Uploadify extension method. function initUploadify(control, uploadUrl, baseUrl, fileExtensions, fileDescription, authenticationToken, errorFunction, completeFunction) { var options = {}; options.script = uploadUrl; options.uploader = baseUrl + 'uploader.swf'; options.cancelImg = baseUrl + 'cancel.png'; //TODO: Make this an option? options.auto = true; options.scriptData = { AuthenticationToken: authenticationToken }; options.fileExt = fileExtensions; options.fileDesc = fileDescription; if (errorFunction != null) { options.onError = errorFunction; } if (completeFunction != null) { options.onComplete = completeFunction; } control.fileUpload(options); }
In here, I’ve created a simple JavaScript function that actually calls the uploadify JavaScript plug-in. By using this method instead of using C# to emit the configuration code directly, I’m cutting out a fair amount of tag soup, and I’m wrapping things up in a way that will be easier to change in the future. Hopefully. The down side to this approach is that you have to create a new JavaScript method and include for every plug-in you want to use, but combining the scripts and correctly setting cache headers should reduce the request overhead.
I’m not claiming that this is the best way to do this. In fact, I really hope it isn’t, because I still don’t like it. But I think that I like it better than the approach I took for jqGrid. If you have any suggestions or feedback, please share. Feel free to tell me that I’m doing things completely wrong.
I feel your pain. After reading this I made this little post on SO: http://stackoverflow.com/questions/886688/a-public-open-source-htmlhelper-repository-for-sharing-controls
Maybe you would like to pitch in?
I think this is a good idea, but it’s sort-of already being done under the MVCContrib banner. I have some issues with that project (mainly the bad documentation), but it has a lot of nice helpers and utilities. Still, I think a project that’s a little more structured, focused, and "opinionated" could be very useful. I added a vote to your SO post and added a note or two there.
Given your dislike of HTML Helper UI controls, can you give us your thoughts on why there’s any merit to this approach as opposed to simply rending Uploadify as pure Javascript? This combination of files certainly doesn’t seem easier, esp. if your development team consists of people in clear-cut roles (CSS/ HTML, Javascript, .NET C#).
@Jon,
I don’t dislike HTML Helper UI controls, what I dislike is how the balance of language in the helpers tends to shift from C# to JavaScript or HTML. My goal with this approach was to minimize the use of HTML/JavaScript-as-a-magicing in the HTML Helper, not to eliminate the helper itself. Again, I’m not saying this approach is the best one, I just like it better than the approach I used when integrating jqGrid, which consisted of an insane amount of JavaScript embedded inside the HTML Helper method. To me, that’s just a bad way to do things. It’s not testable, it’s error-prone, and it’s unreadable.
I understand your goal and agree that your implementation is an improvement. However, I think that the HTMLHelper is widely abused by ASP.NET programmers who can’t seem to give up the good old days of client-side code being injected into the page by server-side methods. HTMLHelpers are a great way to form basic HTML tags, but they are easily over-extended.
You might consider using a partial view instead. It may look less compact than an extension method, but at least you’re keeping your HTML and JS within views which are cleanly separated from the back-end logic and accessible to front-end coders. If you’re sick of tag soup, then look into a different view engine such as Spark.
@Jon,
I agree, I think we’re pushing past what is reasonable for an HtmlHelper extension. I actually like (and would use) the partial view approach, but unless I’ve missed something, a partial view cannot be nested inside a library; it has to be part of the MVC application. That makes reuse across applications much more difficult.
Thanks for the tip, I will check out Spark. I confess that I have not looked beyond the WebForms view engine yet. 🙁
If you do look into Spark this might be a good example of something which would be done with a macro.
http://whereslou.com/2009/05/22/spark-macro-for-uploadify-take-one-b