*** Edited on January 30, 2012 – Forgot that FireFox does not support innerText, so I replaced all references to our best friend jQuery’s text() method. ***

Slight detour… I know I’m behind on posting part three of my series on wresting with Telerik’s MVC Grid control, but a lot has happened since I posted part two. One is Telerik’s release of Kendo UI. There’s been some concern (myself, included) that Kendo UI may eventually replace the Telerik MVC extensions (since there are plans to include some MVC-specific server-side wrappers), but they’re currently denying that. I still plan on posting that third article for completeness sake, but I’m not 100% sure of the MVC extensions future.

Also, the more I use it, and the more comfortable I am working down at the metal with JavaScript, HTML, CSS, and jQuery (not metal, but still a de facto standard), the less enamored I am of such a library. Sure, it provides a useful layer of abstraction, much in the same way ASP.NET Web Pages provides a layer of abstraction for building entire sites. But as my series of articles implies, if you want go deeper than what the abstraction provides, you end up fighting against the tool, and it no longer feels right.

The Challenge

I came upon such a situation today. Maybe I missed something, but struggling so mightily with what I’d expect to be handled by a simple attribute, makes me feel the tool is just getting in the way. I had a request from a client for a master / detail grid to retain its expansion state between operations, such as adding, editing, and deleting rows at the master level. My initial thought was, “This should be easy — it’s probably just a parameter I need to check.” Yet, my experience with the grid gave me an uneasy feeling that it wasn’t going to be so simple.

The First Attempt

So, after not finding such a simple setting, I did some web searching. Of course, the two main resources found were Telerik’s forums (they do have pretty good support), and StackOverflow. Not much there, aside for some related issues which gave some hints. It made sense to hook into the grid’s client events and API. It seemed obvious that I’d need to hook into the OnDetailViewExpand and OnDetailViewCollapse events to keep track of the master row expansion state. So knowing where to capture this info was easy. But before knowing what to capture, I had to see what’s needed to restore the state after a data refresh.

That’s what cost me hours poring through a deep hierarchy of properties. Here’s the issue: I figured I needed to call the expandRow API method to restore the previously expanded rows. This method requires a jQuery object parameter representing the master row. Here’s their documentation:

var grid = $("#Grid").data("tGrid");
// get the first master table row
var tr = $("#Grid tbody > .t-master-row:eq(0)"); // use .t-master-row to select only the master rows

// expand the row
grid.expandRow(tr);

Ok, simple enough, although this is an example of exposing too much gunk under the hood — without their sample code, you’d have to do a lot of trial and error to figure out exactly which Telerik classes to use in order to select the pieces that make up their controls. Ok, ignoring that, it still should have been straightforward. It seemed the good news was that both OnDetailViewExpand and OnDetailViewCollapse received an event parameter giving us access to a masterRow jQuery object field. Since the expandRow call requires such a parameter, I thought I was set.

Nope. Their example worked fine, selecting the row via $(“#Grid tbody > .t-master-row:eq(0)”). But using the retained masterRow reference, absolutely nothing happened. No JavaScript errors, mind you. Just no expansion.

I spent way too much time inspecting every inch of these two seemingly identical structures, single-stepping through both Telerik’s and jQuery’s JavaScript code, and not seeing anything significantly different. Finally, I decided to take a different approach that we’ll discuss below.

The Successful Solution

As mentioned, we’re going to have to hook into the OnDetailViewExpand and OnDetailViewCollapse grid events so we can capture the state of each row. We’ll also need to hook into the OnRowDataBound grid event, because as we traverse through the rows as we bind, we’re going to expand the rows that were previously expanded. Here’s the section of the Razor view page where we define the event hooks we’re going to make use of:

.ClientEvents(events => events
    ...
    ...
    ...
    .OnRowDataBound("onRowDataBound")
    .OnDetailViewExpand("onExpand")
    .OnDetailViewCollapse("onCollapse"))

We’re going to use an array to track the expanded rows. I wrote a few utility functions to support the array:

  • addToArray, which we’ll use on expand, and will only add an element if it doesn’t already exist.
  • removeFromArray, which we’ll use upon collapse.
  • isInArray, which returns true if the element already exists.

You may know a better way to handle this in JavaScript, but these generic and reusable functions work nicely, so we’ll go with it.

The rest of the code is just as simple:

  • Since the innerText of each row should be unique, we’re going to use that as the value we push onto the array in the onExpand handler. We can get this value through a field of the masterRow event argument.
  • In the onCollapse handler, we’re going to remove this value from the array. We can also make the same call before deleting a master row from the grid, but I don’t show that here.
  • Finally, in the onRowDataBound handler, we first need to grab a reference to the master grid. We check if the innerText value is already in the array, and if so, we make a call to expandRow, passing in the row event argument. For some reason, this works fine, although as I mentioned earlier, using supposedly the same object type (the entire e.masterRow event argument) doesn’t work. *Shrug*
var expandedRows = [];

function onExpand(e) {
    addToArray(expandedRows, $(e.masterRow).text());
}

function onCollapse(e) {
    removeFromArray(expandedRows, $(e.masterRow).text());
}

function onRowDataBound(e) {
    var grid = $("#CustomerGrid").data("tGrid");
    if (isInArray(expandedRows, $(e.row).text())) grid.expandRow($(e.row));
}

function addToArray(arr, value) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] === value) return;
    }

    arr.push(value);
}

function removeFromArray(arr, value) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] === value) {
            delete arr[i];
            return;
        }
    }
}

function isInArray(arr, value) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] === value) return true;
    }

    return false;
}

Alternatives

I’m becoming less of a fan of these kind of libraries, where a different coding paradigm is used to try to “simplify” things. In my mind, keeping it simple means keeping to well-known paradigms, unless a new one has tremendous benefit. This is why I’m intrigued by a jQuery plug-in called DataTables. It uses basic JavaScript constructs to specify customizations.

Although not a grid, another powerful jQuery plug-in I’ve been using is a treeview control called DynaTree. Although it can be pretty complex, at least it takes advantage of straightforward, standard JavaScript and jQuery constructs.

Upon cursory review, Kendo UI appears to take a more bare-bones approach as well, taking advantage of HTML5, JavaScript, and CSS, allowing for more control, popular for us ASP.NET MVC fans. I’ll look into this deeper, and perhaps write about it in the future. But for now, I’ve invested a lot in the Telerik MVC library for a large project, so I’m sort of stuck with it.