Aug 21 2009

liteGrid’s new provider model for layout

Category: jQuery | liteGridMatt @ 08:22

I just committed a major change to liteGrid.  Prior to this change, liteGrid’s core was responsible for most of the table rendering.  It would render a basic table with a header and no rich-UI functionality.  All the niceties (resizable columns, jQuery UI integration, fixed header row) were added by the LayoutManager module.  This worked, but required a few ugly hacks, such as shifting the table behind the fixed header row.  It also introduced some module-to-module coupling issues that I didn’t like.  Some modules (such as the sortable columns one I’m working on) really need to know about the rich UI added by the LayoutManager module.  I solved part of this by migrating some things, like the jQuery UI placeholders, into liteGrid’s core, but that never felt like the right solution to me.

Changing gears for a second, I always envisioned basically three ways to extend liteGrid.  The first is obvious: the add-on modules.  The second is providers that could be plugged in.  The third is by replacing built-in methods with your own.

Providers are different from modules in that they are *required* for liteGrid to function, they have a fixed spec that they must implement, and modules can reference them because they are guaranteed to be there.  Originally I had envisioned a format provider in addition to the data provider that exists today, but I ended up scraping it.  So prior to today, there was only a single provider slot (the data provider). 

Back to today’s changes: liteGrid now has a second provider that handles the layout of the grid.  That’s right, liteGrid core now contains now rendering logic.  It stubs out the rendering methods with exceptions (so that developers will get a more useful error message if they fail to implement a layout provider correctly), but it defers all the rendering to the configured layout provider.  The default provider replaces the LayoutManager add-on and includes resizable columns, a fixed header row, etc.  Since it has complete control over the markup that’s rendered, it also removes a few ugly IE-specific hacks, and makes it easier to add sorting to liteGrid.

Another cool thing about switching to a provider model is that you can now fully control how liteGrid is rendered (though there might still be a few modules that need to be updated to make this statement completely true).  Want to render it as a bunch of nested <div> elements?  Go right ahead.  Want to spit it out as an unordered list?  That’s doable, too.

Tags:

Aug 18 2009

liteGrid Prototype Demo

Category: liteGridMatt @ 03:13

I have created a simple prototype to show off some of the basic features of liteGrid.  The demo is available on SVN, or you can access it online here.  As liteGrid uses a data provider model, I had to implement a fake provider (see demoProvider.js).  It maintains a pseudo-random in-memory database of widgets on the client.  In order to simulate the time a normal AJAX request might take, all calls to the provider have a 500ms delay.  Again, even though this is not release-quality yet, feedback is still welcome and appreciated. 

Tags:

Aug 17 2009

liteGrid code now available!

Category: liteGridMatt @ 08:44

The source code for liteGrid is now available on Google Code!  Note that this is *NOT* a release, it’s just an announcement that the code is now available, as promised.  I still have several things I want to do to the code base before I consider it release-worthy.  If you are interested in helping out, please drop me a line.  If you run into any issues, feel free to log them on the Google Code issue tracker.  Generic feedback is also very welcome, feel free to leave it as a comment here or E-mail me directly.

Tags:

Aug 13 2009

What&rsquo;s up with liteGrid?

Category: liteGridMatt @ 09:49

Tons of progress has happened with liteGrid since my last in-depth post.  At this point, I think I’ve met all the goals I defined, and I’m very happy with it, in terms of both functionality and design.  Here’s what the current set of modules provides (remember all of this is mix-and-match, so you can tailor it to what you need):

  • AJAX-based data provider supporting retrieval, updates (both multi-row batch and single-cell modes), updates, and inserts, with full support for retrieving and rebinding database-populated values post insert/update (more on this in a future post)
  • Batch saving module that allows users to persist their changes on-demand
  • BlockUI integration, nicely disabling access to the grid during data operations
  • Cell saving module that automatically saves changes whenever a cell is edited
  • Formatting module that allows you to specify anything from simple to advanced formatting rules
  • Editable via integration with JEditable
  • Layout manager that adds resizable columns, fixed header row, and more
  • Integration with jQuery UI
  • Row addition module that does exactly what it sounds like
  • Row deletion module that adds a delete button to all rows
  • (Thanks to David Koehler) Injection protection through escaping potentially dangerous characters, and unescaping them prior to edit
  • Dynamic row striping that maintains the correct striping even as the grid changes
  • Toolbar that supports custom buttons
  • Tree-grid functionality, allowing rows to be nested under other rows

All of this functionality is implemented using liteGrid’s event-driven architecture, so there is little/no coupling between the core and the various modules. 

Like I said a while back, all this is Coming Soon to an open-source repository near you.  My plan is to make that happen within the next week.  I don’t plan to make a release right away, but the code will be there for anyone that wants to take a look. 

Roadmap to V1.0

Here are the list of things I want to do before I’ll be comfortable calling this a true “release”:

  • Unit tests (probably using JSUnit)
  • Better documentation; the code is well-commented, but I’d like to add vs-doc support as well as have a rich wiki that describes everything
  • Complete demo application (including ASP.NET MVC backend)
  • Resolve any lingering Internet Explorer 8 rendering issues

If you are interested in contributing, drop me a line, and I’ll let you know as soon as the source is available for you to check out.  Or, stay tuned to this blog, and watch for a link to the project within the next week (hopefully).

Tags:

Aug 11 2009

Standardizing JSON Responses in ASP.NET MVC

Category: ASP.NET | MVC | liteGridMatt @ 04:05

ASP.NET MVC provides very nice support for returning JSON data, but my chief complaint with it is that it’s too flexible.  You can basically cram anything you want in it and trust that it will make it to the client script, which has lead to a complete lack of coherence among the various controllers in our big MVC application.  Some actions return a simple string that contains either “success” or “error” depending on whether or not the operation succeeded.  Others return true or false.  This is going to cause maintenance problems down the road, so we are taking a stab at standardizing things.  Right now, the idea is to create a class derived from JsonResult that exposes a few standard properties while at the same time maintaining the flexibility of the original JsonResult.  The common properties are “status”, a boolean that is true or false depending on success or failure of the requested action, and “message”, an optional string that can be set with additional details in the case of failure.  The custom result maintains the ability to insert additional properties into the JSON result via an object (anonymous or otherwise) as well as through adding key/value pairs explicitly.  The implementation is quite simple (commments removed for readability):

public class StandardJsonResult : JsonResult
{
    public string Message { get; set; }

    public bool Status { get; set; }

    public Dictionary<string, object> Properties { get; private set; }

    public StandardJsonResult()
    {
        Properties = new Dictionary<string, object>();
        Data = Properties;
        Status = true;
    }

    public StandardJsonResult(object data)
        : this()
    {
        IDictionary<string, object> properties = data.ToDictionary();

        foreach (var keyValue in properties)
        {
            Properties.Add(keyValue.Key, keyValue.Value);
        }
    }

    public StandardJsonResult(Exception ex) : this()
    {
        Status = false;
        Message = ex.Message;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        //Copy in standard properties
        Properties.Add("status", Status);

        if (Message != null)
        {
            Properties.Add("message", Message);
        }

        base.ExecuteResult(context);
    }
}

There are several constructors provided.  The one that takes an object as a parameter allows you to send anonymous objects or view models across the wire just like you can with a regular JsonResult object.  The values are copied from the object using the method I previous described, but it could also be done using a RouteDataDictionary.  The values are added to the Properties dictionary, which is actually assigned to the JsonResult’s Data property.  The key/value pairs in the dictionary are serialized to properties in the JSON output.  The ExecuteResult method is overridden so that the two standard properties can be added to the dictionary prior to JSON serialization.

This class can also be extended for other recurring scenarios.  For example, here is the custom result that feeds data into liteGrid:

public class LiteGridJsonResult : StandardJsonResult
{
    public Array DataItems { get; private set; }

    public LiteGridJsonResult(Array dataItems)
    {
        DataItems = dataItems;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        Properties.Add("dataItems", DataItems);

        base.ExecuteResult(context);
    }
}

This custom result simply extends the standard one with a collection of data items that will be rendered by the client. 

Thoughts or suggestions?

Tags:

Aug 3 2009

Integrating jEditable with liteGrid

Category: jQuery | JavaScript | liteGridMatt @ 07:37

Work continues on liteGrid (I believe that’s the name I’m going to stick with), and in fact so much has changed that I really don’t even know where to start when talking about it.  The core has been changed around a bit (for the better), many new modules have been added, an AJAX data provider has been added… it’s been a busy couple of weeks.  Today, I thought I would start with something fairly straight-forward: a new module that brings the power of jEditable into liteGrid. 

If you recall my previous post, I basically wrote all the cell-editing script from scratch.  It worked well enough and was extensible, but as I noted in my article, James Kolpack pointed out that I really did a lot more work than was necessary.  So, in what little “spare time” I have, I implemented a module that achieves the same result (click-to-edit) using jEditable instead of custom script.  This has several advantages.  First, jEditable has a fairly rich set of editors already, and there are even 3rd party add-ons for additional editor types.  Second, the goal of liteGrid isn’t editing, the goal is to provide a flexible, extensible, and lightweight grid.  Maintaining my own editing library wasn’t going to help me achieve that goal.  So, the old InlineEditingModule was thrown away, and JEditableModule has taken it’s place. 

Let’s start with the high-level summary first:

function JEditableModule() {

    //Key codes.
    var enterKey = 13;

    var base = this;

    //Registers for events.
    base.initialize = function(liteGrid, options) {
        ...
    }

    //Attaches jEditable to editable columns.
    base.columnBound = function(event, column, tdElement) {
        ...
    }

    //Callback that is run whenenever a cell has been saved.  This
    //stores the cell value in the underlying data item.
    base.saveCell = function(value, settings) {
        ...
    }

    //Callback that is run after a cell's value has been changed. 
    base.afterSave = function(value, settings) {
        ...
    }
}
JEditableModule.defaultOptions = { placeholder: "", onblur: "submit", type: "text" };

There is the usual initialize function that all liteGrid modules must define.  Next is an event handler that fires when columns are bound.  Finally, there are two helpers: one that is responsible for actually updating the underlying dataItem when a value changes, and one that is called after a cell has been updated.  There are also some default options that can be overridden by the liteGrid options (as we’ll see in a second).

The magic begins in the initialize function:

base.initialize = function(liteGrid, options) {
    base.liteGrid = liteGrid;
    base.options = options;

    //If jEditable isn't defined, we can't do anything.
    if (!$.editable) {
        console.log("Unable to initialize, can't find the jEditable plug-in.");
        return;
    }

    liteGrid.$el.bind("columnBound", base.columnBound);
}

If the jEditable plug-in isn’t available, initialization is aborted, and an error is logged to Firebug.  Otherwise, the module registers for columnBound events:

base.columnBound = function(event, column, tdElement) {

    //If the column isn't editable, or if we've already applied 
    //jEditable, don't do anything.
    if (column.editable !== true || tdElement.hasClass("editable")) {
        return;
    }

    //Additional options are stored in the settings, making them available to callback functions.
    var options = $.extend({}, JEditableModule.defaultOptions, { callback: base.afterSave, column: column, tdElement: tdElement });

    if (column.type) {
        //If the type isn't supported, alert the user.
        if (!$.editable.types[column.type]) {
            console.warn("Unable to find editor for type " + column.type + " in jEditable.");
            return;
        }

        options.type = column.type;
    }

    //Special-case: the built-in select editor requires additional properties that define the options.
    if (column.type == "select") {
        options.data = column.selectOptions;
        //This will end edit mode when the user presses enter.
        tdElement.keyup(function(event) { if (event.keyCode == enterKey) $("select", tdElement).blur(); });
    }

    //Make the element editable.
    tdElement.editable(base.saveCell, options);
    tdElement.addClass("editable");
}

If the column isn’t editable, or if it has already been processed (as indicated by the marker class “editable”), nothing is done.  Otherwise, an options object is built up that will be passed on to jEditable.  If the column type doesn’t have a defined editor in jEditable, a warning is logged, and processing terminates.  While jEditable likes to post changes to a URL via AJAX by default, it also supports a callback to handle the save, which is leveraged here with base.saveCell.

For “select” types, which render as dropdown lists, a couple of extra steps are required.  First, the options for the select are copied from the column definition.  I’m not completely satisfied with this approach as I do not like how jEditable requires you to specify your columns, but it works (for now).  Second, the standard textbox editors persist their changes when the user hits the enter key.  That doesn’t happen for dropdown lists, so a function is attached that triggers the blur event for the select, thereby triggering the value to persist. 

The saveCell function is called by jEditable when the user has indicated that they want to persist a new value:

base.saveCell = function(value, settings) {

    var cell = $(settings.tdElement);

    //See if the value actually changed
    var dataItem = cell.parent().data("dataItem");

    var currentValue = dataItem[settings.column.field];

    //An event is raised so that interested parties can modify the
    //value prior to attempting to persist it.  
    //TODO: ADD HOOKS FOR VALIDATION!
    var event = $.Event("valueChanged");
    event.currentValue = currentValue;
    event.newValue = value;
    event.column = settings.column;

    base.liteGrid.$el.trigger(event);

    //Subscribers may have modified the new value
    value = event.newValue;

    //If the value hasn't changed, or if the value is still null/empty, don't do anything.
    if (currentValue == value || ((currentValue || null) == null && value == "")) {
        settings.valueChanged = false;
    }
    else {
        //Mark the cell as having been changed.  This is used by the 
        //callback handler.
        settings.valueChanged = true;
        cell.addClass("modified");

        dataItem[settings.column.field] = value;
        cell.parent().data("dataItem", dataItem);
    }

    return value;
}

The underlying data item is retrieved from the parent row so that the new value can be compared to the current value.  An event is fired with column, new value, and current value.  This allows interested parties to modify the value if they so choose.  Ideally, validation could also be handled here, but I haven’t added that (yet).  A simple check is performed to see if the new value has changed.  If so, the cell is marked as changed, and the data item is updated.

After jEditable has saved the new value using the saveCell method, it calls the afterSave function:

base.afterSave = function(value, settings) {
    if (settings.valueChanged == true) {
        base.liteGrid.$el.trigger("columnBound", [settings.column, settings.tdElement]);
    }
}

This callback looks to see if the value actually changed, and if so, raises the columnBound event.

And that’s it.  It’s considerably simpler than the old method, and aside from a few rendering bugs with IE 8 that I haven’t ironed out yet, I don’t see any reason to use the old InlineEditModule instead of this new JEditableModule. 

Crowd: “This is all well and good, but let’s see it in action!”

Yeah, I’m working on that.  Once I get it migrated to Google Code, I’ll stand up some demos that people can play around with.  Until then, you’ll just have to believe me when I tell you that it works.

Tags: