Ajax HTML Grid Control for ASP.NET MVC (Part 2)

Seth Juarez
Seth Juarez7/8/2008

Preamble

Over the long weekend I thought a lot about where I wanted to go with the grid "control" for ASP.NET MVC. One of the things that weighed heavily on my mind was the ability to have the control fully customizable. As I thought about this, I decided that first thing's first: I need to get the functionality working and then worry about the prettiness factor. So for those of you concerned about the customizability - it's coming. For now, I really want to focus on the ability of the grid to get work done.

Code Bloat

As I began to dive again into the previous code I realized that having everything in one extension method was going to be... well ugly. The first order of business was to abstract everything in to a separate DLL. In order to do so, I created a new Grid class that handles all of the grid drawing.

JavaScript

I am partial to using JQuery. So here is the general idea of what the JavaScript code needs to do:

  1. Detect that the user would like to edit a particular row
  2. Detect any rows previously being edited (to move the edit focus to the new row being edited)
  3. Display text boxes for the user to have the ability to edit the values
  4. Post back an edit or a delete

In order to do each of the items above, I needed to change what the grid was outputting a little. First I needed to wrap the table in a

tag in order to retrieve any valid values being edited. Also, I needed to add the following links: Edit, Delete, Cancel, Save. The first two (Edit, Delete) should be shown when a row was not in edit mode. The second two (Cancel, Save) should only be shown when a row is in edit mode. I also wanted to make sure that only one row was being edited at a time. Here is a fragment of the HTML the control produces:

<form name="Student_Form" id="Student_Form">
<table id="Student" style="border: solid 1px black;width:100%;">
<tr id="Student_6">
    <td class="edit">
    <span class="editor">
       <a id="6_Student_edit" href="#">Edit</a> <a id="6_Student_delete" href="#">Delete</a>
    </span>
    <span class="editing">
       <a id="6_Student_cancel" href="#">Cancel</a> <a id="6_Student_save" href="#">Save</a>
    </span></td><td class="StudentId">6</td>
    <td class="FirstName">Frances</td>
    <td class="LastName">Adams</td>
    <td class="Age"> </td>
    <td class="Address"> </td>
    <td class="City"> </td>
    <td class="State"> </td>
    <td class="Zip"> </td>
    <td class="Phone"> </td>
</tr>
...
</table>
</form>

Once I decided on the DOM elements the form would produce, it was time to put some JavaScript to each row. The first bit of code is designed to attach events to each of the links in the grid as well as hide the "editing" spans since each row by default starts in non-edit mode:

$('document').ready(
    function(){
        $('#Student span.editing').hide();
        $('#Student a').click(
            function(event){
                event.preventDefault();
                handleEditClick(this.id);
            }
        );
    }
);

This code ensures that the editing spans are hidden and each click event on the anchor tags are redirected to the handleEditClick function. Notice that I pass in the id of the actual anchor tag. These I defined as id_Grid_action (see above). Whenever a link is pushed I get those three pieces of information to proceed with processing. Now for the handleClick function (it is long):

function handleEditClick(itm) {
    var o = itm.split('_');
    var id = o[0];
    var grid = o[1];
    var action = o[2];
    var name = '#' + grid + '_' + id + ' td';

    if(action == 'edit')
    {
        // un-edit any others that might be in editmode
        $('#Student span.editor:hidden a:first').each(
            function() {
                var cl = this.id.split('_');
                $('#' + cl[0] + '_' + cl[1] + '_cancel').click();
            }
        );

        $('.editor', name).hide();
        $('.editing', name).show();

        $(name).each(
            function() {
                if($(this).hasClass('edit')) return;
                var data = $(this).text() == ' ' ? '' : $(this).text();
                $(this).html('<input type="text" name="' + 
          $(this).attr('class') + 
          '" value="' + 
          data + 
          '" size="10" />');
            }
        );
    }
    else if(action == 'cancel')
    {
        $('.editor', name).show();
        $('.editing', name).hide();
        $(name).each(
            function() {
                if($(this).hasClass('edit')) return;
                var data = $('input', this).val();
                $(this).html(data == '' ? 'nbsp;' : data);
            }
        );
    }
    else if(action == 'save')
    {
        //alert('Save ' + itm);
        var arr = $('#Student_Form').serializeArray();
        $.each(arr, 
            function(i, field) {
                alert(field.name + ': ' + field.value);
            }
        );
    }
    else if(action == 'delete')
        alert('Delete ' + itm);

As advertised, I first break up the id\_grid\_action pair into variables that will be useful. Also notice that ALL actions come into this function. First lets focus on the edit action. Now this is why I love JQuery:

$('#Student span.editor:hidden a:first').each(...

This particular line of code selects the first anchor tag under a span with class editor that is hidden from the Student grid. Why would I want to do that? From the id of the anchor tag I can reconstruct the id of the cancel anchor tag and then "click" it in order to cancel the update. To do it any other way would be difficult (at least I think so). The elegance of JQuery allows for those kinds of things. Once we cancel any other edit, we proceed to go through each TD in the row in question (with the exception of the edit anchors) and push the data into text boxes. Also, we set the appropriate edit spans to visible and hidden in order to have the correct actions displayed. Next, the Cancel action. The job of the cancel action is to take the data out of the text boxes and stuff them back into the TD tag. I realize that I should probably add a "Do you want to save this?" confirm box, but I will leave that for later. Notice again the elegance of JQuery:

var data = $('input', this).val();

The $(INPUT, TD) functions as a selection within a previous selection. In other words, within the current TD, find me an INPUT HTML element and retrieve the value. Once we retrieve the value, we can put it back into the TD tag without the INPUT element.

The Grid Class

The first thing I did was write the JavaScript code with the output from the Grid class. In other words, I ran the flat table (without the JavaScript) and cut and pasted the table to a standard HTML file. Once I had the new HTML file I worked on the JavaScript until it worked as I expected. Now the only problem left was creating  a RenderJavaScript() that emitted the Grid specific JavaScript we needed. Doing that seemed a bit tedious so I downloaded a nifty little Add In that did it for me.

Outcome

Some screens:

Screen

Usage

Now that I've abstracted the Grid out to a completely separate project, I changed the helper class to this:

public static string ToAjaxGrid<T, TKey>(this IEnumerable<T> list, 
                                 Func<T, TKey> key, 
                                 string name)
{
   Grid<T, TKey> grid = new Grid<t ,TKEY>(list, key, name);
   return grid.Render();
}

This change allowed me to leave the code in the View the same:

<%= 
    ViewData.Model.ToAjaxGrid<ajaxhelpers.models.studententity , int>
    (s => s.StudentId, "Student") 
%>

Next Time

I think the client side functionality is (mostly) done. For the next installment I will try to actually submit the requested actions to the MVC controller (save, delete).

Your Thoughts?

Let me know if you have a question/thoughts about "Ajax HTML Grid Control for ASP.NET MVC (Part 2)"!
  • Does it make sense?
  • Did it help you solve a problem?
  • Were you looking for something else?
Happy to answer any questions!