Part 1 – The LiteGridJsonResult

In the last post, I showed how we use a custom JsonResult to send data to liteGrid.  Displaying data is great and all, but liteGrid supports client-side editing as well.  With the BatchSaveModule, liteGrid will batch up all the client-side changes and send them to the server in one big lump.  We need an easy, reusable way to process and apply those changes server-side.  Thankfully we now have the LiteGridUpdater class:

public abstract class LiteGridUpdater<TModel, TTarget> 
    where TModel : ILiteGridUpdateModel<TTarget> 
    where TTarget : class, new()
{
    /// <summary>
    /// Applies updates from the view models.
    /// </summary>
    /// <param name="models">The models containing changes.</param>
    /// <returns>The view models that were applied to the targets.  IDs for new entities
    /// will be synchronized to the real objects.</returns>
    public TModel[] ApplyUpdates(TModel[] models)
    {
        ...
    }

    /// <summary>
    /// Moves the object to a new parent.
    /// </summary>
    /// <param name="model"></param>
    protected virtual void MoveToNewParent(TModel model)
    {
        throw new NotImplementedException("Moving items to a new parent is not supported.");
    }

    /// <summary>
    /// Derived classes can implement this if they want to support
    /// adding new items from liteGrid.  Implementors must perform
    /// three actions: create a new TTarget; apply the updates
    /// from model to the new TTarget; synchronize the ID of the
    /// new TTarget with the model.
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    protected virtual TTarget CreateAndUpdate(TModel model)
    {
        throw new NotImplementedException("Adding new items is not supported.");
    }

    /// <summary>
    /// Derived classes can implement this if they want to support deletes.
    /// If implemented, this should delete the specified target.
    /// </summary>
    /// <param name="model">The view model.</param>
    /// <param name="target">The object that should be deleted.</param>
    protected virtual void DoDelete(TModel model, TTarget target)
    {
        throw new NotImplementedException("Deleting items is not supported.");
    }

    /// <summary>
    /// Called before a model is updated.  You can transform the 
    /// model by overriding this method.
    /// </summary>
    /// <param name="model"></param>
    protected virtual void BeforeUpdateModel(TModel model)
    {
    }

    /// <summary>
    /// Implemented by derived classes, used to find the specific object
    /// that should be updated. 
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    protected abstract TTarget GetTarget(TModel model);
}

This generic abstract class will take the data from liteGrid in the form of view models and apply the changes to your “backend” data types.  There are only two restrictions.  First, the view model must implement the ILiteGridUpdateModel interface (shown below), and the backend type must have a parameterless constructor. 

The LiteGridUpdater is implemented using the Template Method Pattern.  The core logic of applying the updates is implemented in LiteGridUpdater:

public TModel[] ApplyUpdates(TModel[] models)
{
    if (models == null)
    {
        throw new ArgumentNullException("models", "No models were received by the server.  Please try again.");
    }

    //First, apply any updates and moves. Note that because liteGrid sends things in order,
    //we are guaranteed to add a new object before we'll process anything that was added as a child
    //to the new object.
    foreach (var updatedModel in models)
    {
        BeforeUpdateModel(updatedModel);

        //Get the existing object or create a new one.
        TTarget target = GetTarget(updatedModel);

        if (target != null)
        {
            updatedModel.MapTo(target);
        }
        else
        {
            CreateAndUpdate(updatedModel);
        }

        //Apply any moves
        if (updatedModel is ILiteGridMovableModel<TTarget>
            && ((ILiteGridMovableModel<TTarget>)updatedModel).NewParentId.HasValue)
        {
            MoveToNewParent(updatedModel);
        }
    }

    //Now apply any deletes.
    foreach (var updateModel in models)
    {
        if (updateModel.Deleted == true)
        {
            DoDelete(updateModel, GetTarget(updateModel));
        }
    }

    return models;
}

Derived classes are responsible for implementing the details for how to perform the various operations, which include create a new entity, deleting an existing entity, and moving an entity (in the case of hierarchical entities).  Derived classes don’t necessarily have to implement all the operations.  Obviously you don’t need to worry about MoveToNewParent if your entities aren’t hierarchical.  If you aren’t allowing the users to add new rows, then you don’t need to worry about implementing the CreateAndUpdate method, either.  The same goes for DoDelete.  The BeforeUpdateModel method is optional.  You can use it to hook in and perform custom actions prior to applying updates (we use it to hook in validation).  In fact, the only operation you have to implement in a derived class is GetTarget.  This operation receives a view model and is responsible for locating the corresponding backend entity and returning it.  In our case, this involves looking the entity up using Castle ActiveRecord, but LiteGridUpdater is not coupled to any data access strategy.

Updates to existing entities are handled by the view model.  The ILiteGridUpdateModel defines a single method for performing the updates on an entity:

public interface ILiteGridUpdateModel<TTarget>
{
    /// <summary>
    /// True if the model was deleted. 
    /// </summary>
    bool? Deleted { get; }

    /// <summary>
    /// Maps the view model to the specified target.
    /// </summary>
    /// <param name="target"></param>
    void MapTo(TTarget target);
}

We use AutoMapper in our concrete liteGrid view models, but you are free to implement the update logic as you see fit.  Here’s a made up example for a Widget:

public class WidgetViewModel : ILiteGridUpdateModel<Widget>
{
    public int WidgetId { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public bool? Deleted { get; set; }

    /// <summary>
    /// Configures AutoMapper mappings.
    /// </summary>
    static WidgetViewModel()
    {
        Mapper.CreateMap<WidgetViewModel, Widget>();
    }

    /// <summary>
    /// Creates an uninitialized view model
    /// </summary>
    public WidgetViewModel()
    {
        
    }

    /// <summary>
    /// Maps the view model to the specified target.
    /// </summary>
    /// <param name="target"></param>
    public void MapTo(Widget target)
    {
        Mapper.Map(this, target);
    }
}

After LiteGridUpdater applies the updates, it returns the same view models that it received.  These should be pumped back to liteGrid so that it knows what changed and can update its state accordingly.

Prior to creating LiteGridUpdater, we had quite a bit of repeated, complex logic in our system for dealing with liteGrid updates.  Now that we have LiteGridUpdater, we have a cleaner, more robust solution, and a lot less duplicate logic. 

Again, I really need to find the time to move LiteGridUpdater as well as some of our other liteGrid infrastructure into the liteGrid project as I originally intended.  Maybe someday soon I’ll find that mythical block of “free time” that everyone else seems to be enjoying.