May 13 2009

ASP.NET MVC HtmlHelper for Uploadify, Take One

Category: ASP.NET | MVC | JavaScriptMatt @ 06:56

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.

Tags:

May 6 2009

Prevent users from abandoning changes accidentally with jqGrid

Category: JavaScriptMatt @ 05:22

In the project I’m working on, we use jqGrid to display hierarchical records in an Excel-like manner.  Changes are queued up on the client and submitted to the server whenever the user clicks the “Save” button.   That means a user could make a bunch of changes, then navigate away from the page accidentally, and lose everything they just did.  Preventing that is easy enough though, just use this JavaScript snippet:

   1: window.onbeforeunload=checkForChanges;
   2: function checkForChanges(){
   3:     var rows = $('#treeTable').getChangedCells('all');
   4:     if (rows != null && rows.length > 0) {
   5:         return 'You have unsaved changes that will be discarded.';
   6:     }
   7: };

The checkForChanges function is bound to the onbeforeunload event, which is fired whenever the browser window is about to change the page.  This includes page reload/refreshes or anything like that.  The checkForChanges function uses the jqGrid getChangedCells method to see if there are any unsaved changes.  If there are, it returns a message that will be shown automagically in a JavaScript confirm dialog.  If there aren’t, it doesn’t return anything, and the user is allowed to navigate without being nagged.

Tags:

Nov 18 2008

A long time ago, in a GridTreeView far, far away&hellip;

Category: ASP.NET | JavaScriptMatt @ 15:37

Some of you (and by some, I mean two) have been axiously awaiting the release of the code for the GridTreeView that I described a while back. Thanks to the generosity of my current employer, I’m happy to present the code, a compiled DLL, and a demo ASP.NET MVC site that you can use to test out the GridTreeView.  Enjoy!

Binaries

Source Code

Tags:

Oct 8 2008

Creating a reusable grid tree view with ASP.NET MVC and jQuery

Category: ASP.NET | JavaScript | MVCMatt @ 08:45

I think it is a safe assumption that every web developer has had to display tabular data at one point or another.  Tabular data is easy with ASP.NET: bind a GridView to a data source, and you're all set.  But with ASP.NET MVC, things are a little trickier.  We don't have access to all the nice WebForms controls.  Still, it's fairly easy to do: just write a for-loop, or better yet, use the grid helper from MvcContrib.

Things get a trickier though if your tabular data is also hierarchical.  Typically, we display hierarchical data in a tree of some kind, but trees really aren't great for tabular data.  What would be great is to combine the two somehow.  Fortunately, there's a nice plug-in for jQuery that does just that: ActsAsTreeTable.  It's easy enough to use; all you have to do is embed ID's and CSS class information in your table rows, and the JavaScript does everything else.  Here's a simple example from the docs:

   1: <link href="path/to/jquery.acts_as_tree_table.css" rel="stylesheet" type="text/css" />
   2: <script type="text/javascript" src="path/to/src/jquery.acts_as_tree_table.js"></script>
   1:  
   2: <script type="text/javascript">
   3:     
   4: $(document).ready(function()  {
   5:     $("#your_table_id").acts_as_tree_table();
   6: });
</script>
   3:  
   4: ...
   5:  
   6: <table id="tree">
   7:   <tr id="node-1">
   8:     <td>Parent</td>
   9:   </tr>
  10:   <tr id="node-2" class="child-of-node-1">
  11:     <td>Child</td>
  12:   </tr>
  13: </table>

We can now combine this with the grid from MvcContrib to produce a collapsable Grid Tree View.  This example is encapsulated inside a view user control so that it can be used on any page.  It displays imaginary "widgets" in a tree.  The widgets aren't really hierarchical, so I've fudged it by making it appear that each widget is a child of its predecessor in the table.

First, let's create the grid:

   1: <%
   1:  Html.Grid(GetWidgets(), new Hash(id => ClientID, style => "width:100%"), 
   2: column =>
   3:       {
   4:           column.For(w => w.Name);
   5:           column.For(w => w.Description);
   6:           column.For(w => Html.TextBox("Description_" + w.Id, c.Description), "Editable").DoNotEncode();
   7:       },
   8: sections =>
   9:     {
  10:         sections.RowStart(c =>
  11:                               {
%> <tr class="child-of-node-<%=w.Id - 1%>" id="node-<%=w.Id%>"> <%
   1:  
   2:                               });
   3:     }    ); 
%>

That probably looks horrendous, so let's walk through it.  GetWidgets() is a method on the view user control that grabs widgets from wherever (in practice, probably the model or view data).  Next, the Hash just contains key/value pairs that are embedded in the opening table tag; here, we've specified the table's ID (by using ClientID, it will have the name that ASP.NET gives to the user control), and we've specified that it should be 100% wide.  Next, we define the columns using lambda expressions.  The first two columns simply display the widget's name and description.  The last column is a little more complicated.  It creates a text box using the TextBox helper method.  Since the column contains HTML that shouldn't be encoded, we call DoNotEncode on it.  Finally, we use a lambda expression to override how rows are created.  The code here populates the row with the 'child-of-node-#' class and the id attribute, both of which are needed by ActsAsTreeTable.  It may look intimidating, but it's actually nice once get comfortable with the syntax. 

The last thing we need to do is spit out the JavaScript to turn our gird into an ActsAsTreeTable:

   1: <script type="text/javascript">
   2:         $(document).ready(function()  {
   3:             $("#<%=ClientID %>").acts_as_tree_table();
   4:         });
   5: </script>

If you set everything up correctly, you should now have a working "grid tree view".  In a future article, I'll introduce a new Html helper that does all the heavy lifting for you.

Tags:

Sep 12 2008

Why var is a very important keyword in a recursive JavaScript function...

Category: JavaScriptMatt @ 01:08

I actually don't hate JavaScript.  I'm typically not a fan of duck-typed languages, and JavaScript has caused me no small number of headaches, but it has its uses, and it beats the alternative.  I could be writing in VBScript, after all...

Still, there are a few things about JavaScript that really, really tick me off.  One of those just bit me badly this morning.  Take a look at the following block of code:

   1: //Recursively sets the nodes and their children to the
   2: //specified checked state.
   3: function UpdateChildren(nodes, checked)
   4: {
   5:     for (i=0; i < nodes.get_count(); i++)
   6:     {
   7:         var node = nodes.getNode(i);
   8:         
   9:         node.set_checked(checked);
  10:         
  11:         if (node.get_nodes().get_count() > 0)
  12:             UpdateChildren(node.get_nodes(), checked);
  13:     }
  14: }

See anything wrong with that?  Seriously, stop and look at it before continuing any further.  I stared at that code for about an hour before I finally figured out what was wrong. 

The code looks pretty innocuous.  It's a recursive method that walks a tree, basic stuff.  The problem though, is that the recursion was never ending.  It became stuck in a cycle where it was repeating the same recursive calls over and over, and I couldn't figure out why.  It looked like the recursion was "leaking" somehow.  Turns out, that's actually what was happening, but it wasn't JavaScript's fault.  I left the 'var' keyword out of the 'i=0' assignment.  That means that 'i' is created a global variable instead of a local variable!

I would *much* prefer that JavaScript have given me an error when I tried to assign a value to 'i' than just saying "meh, I'm sure you meant that to global."  Whoever thought that was a good idea obviously hates humanity and wanted to spread agony and sadness.  <sighs>, oh well, back to work.

NOTE: there won't be an installment of "how to run a software company (into the ground) this week because I am running way behind.  Sorry!

Tags: