Saturday, December 19, 2009

jqGrid + ASP.NET MVC: quick and powerful

Dedicated to my wife. 
Thanks for the patience.

In this post I'll explain (or at least will try to, in this and next posts) how to combine jqGrid and ASP.NET MVC to provide a powerful and easy to use solution to quickly create very complex CRUD pages. This is a real code; it was developed as part of a real project - a data-driven admin interface. Note that it is not jqGrid neither ASP.NET MVC guide; it's just a proof of concept that may help to simplify CRUD development.

It includes:
- NHibernate base repository that provides methods for generic paging, sorting, filtering, and selected item lookup;
- Two kinds of ASP.NET MVC base controllers - one for entities, another for entity collections - that can handle basic CRUD methods with minimal required overrides;
- Set of jqGrid C# classes to handle jqGrid communication and to generate jqGrid colModel from C# models, using DataAnnotation and couple of custom attributes;
- Helper jQuery function that handles most of the common jqGrid setup.

It took me about 3-4 days to implement all of this. It's not a framework; it's just couple of helper classes glued together. I won't even provide a working sample project.
However, it really allows to create rich CRUD interfaces in several minutes. To give an impression of what it does, let's quickly create a page to manage this model:

   public class Employee : User // user has Email property
   {
      public Employee(Partner organization)
      {
         Roles = new List<Role>();
         Organization = organization;
      }

      public virtual IList<Role> Roles { get; set; }
      public virtual string Login { get; set; }
      public virtual string Password { get; set; }
      public virtual string Title { get; set; }
      public virtual string LastName { get; set; }
      public virtual string FirstName { get; set; }
      public virtual DateTime? LastLoginTime { get; set; }
      public virtual Partner Organization { get; set; }
      public string Currency { get; set; }
   }

First, I write a view model for this. You can use the business model directly; I prefer to use view models.
It doesn't really matter for our topic, except that it makes differences between pure model and jqGrid-enabled one clearer.

Now, let's add attributes - the main reason this post is born is that a lot of samples in the wild use to define jqGrid model in the JavaScript code, thus duplicating columns definitions and lots of other stuff.
I'm a big follwer of DRY, so I always strieve to eliminate duplication. Thus, defining attributes on the model like below is the ONLY thing I need to do to have both grid with formatting and client and server side validation.

   public class EmployeeViewModel: IJqBasedViewModel
   {
      public static void ConfigureAutoMapper()
      {
         AutoMapper.Mapper.CreateMap<Employee, EmployeeViewModel>()
            .MapFrom(x => x.LastLoginTime == null ? "" : MyDate.Format(x.LastLoginTime.Value), x => x.LastLoginTime)
            .MapFrom(x => x.Roles.Select(r => r.Name).Join(", "), x => x.RolesAll);
         AutoMapper.Mapper.CreateMap<EmployeeViewModel, Employee>()
            .Ignore(x => x.LastLoginTime, x => x.Id, x => x.Roles);
      }

      [Visible(false)]
      public string id { get; set; }

      [UIHint("select", "HTML", "dataUrl", "~/Partners/SelectList")]
      [Visible(false), Required, Display(Name = "Organization")]
      public string OrganizationId { get; set; }
      [Immutable, Display(Order = 1, Name = "Organization")]
      [JqFormatter("partnerLink", "baseLinkUrl", "~/Partners/Show")]
      public string OrganizationName { get; set; }

      public string Title { get; set; }
      [Required]
      public string LastName { get; set; }
      public string FirstName { get; set; }

      [Email]
      public string Email { get; set; }
      [Required]
      public string Login { get; set; }
      [Required, UIHint("password"), Visible(false)]
      public string Password { get; set; }

      [Visible(false)]
      [UIHint("select", "HTML", "dataUrl", "~/Roles/SelectList", "multiple", "true", "size", "5")]
      public IList<string> Roles { get; set; }
      [Immutable, Display(Name = "Roles")]
      public string RolesAll { get; set; }

      [Immutable]
      public string Currency { get; set; }
      [Immutable, Display(Name = "Last Login")]
      public string LastLoginTime { get; set; }
   }

Let's review that. First, a static method defines AutoMapper config. This is my own project's convention; I use that to easily add AutoMapper pieces. Application startup code looks for all view models and calls ConfigureAutoMapper if present. The view model implements IJqBasedViewModel interface; this is just to have
the "id" property which is always required for jqGrid.

AutoMapper maps Organization.Id to OrganizationId, and Organization.Name to OrganizationName. Why we need two?
The first one appears when we edit the row, thus Visible(false). The second one is display in the grid, thus Immutable. UIHint is used to define the control - in this case, dropdown with values populated from the provided dataUrl. JqFormatter is used to format values in the grid; in this case "partnerLink" is our own JavaScript function, but it could be "'showlink'" - a standard jqGrid formatter (notice '').
Both would use row's id to generate the link.
UIHint and JqFormatter support any jqGrid controls and formatters, as well as their parameters. They also apply some processing logic - for example, use Url.Content() for paramters that contain "url".

Other attributes should be self-explanatory. Note that not only they result in jqGrid options applied, but also adds server-side validation (e.g. Required).

Other available attributes and internal behaviour (rendered by JqGrid.cs):
- bool properties generate check images from jQuery CSS framework in the grid, and checkboxes in the edit form;
- enum properties generate drop down lists with values from the enum;
- UIHint where value is typeof(type) that implements IJqGridSelectSource allows to provide custom select values;
- [Range] generates min/max edit rules;
- int generates integer-only edits;
- ExDisplay controls label in edit form and width of column;
- CamelPropertyNames automatically generate "Camel Property Name" column names and edit labels;
- UIHint("date") setup datepicker (with customer initDatePicker function);
- and more.

Now that we defined the view model and how it should look and be edited, left define the controller.

   public class UsersController : JqGridControllerBase<Employee, EmployeeViewModel>
   {
      public UsersController(IUserRepository userRepository)
      {
         this.userRepository = userRepository;
      }

      protected override PageResult<Employee> InternalGet(PagedRequest request)
      {
         request.Override(OrganizationName => "Organization.Name", Roles => "Roles.Name");
         return userRepository.GetAll<Employee>(request);
      }

      protected override void InternalDelete(Employee entity)
      {
         userRepository.Delete(entity);
      }

      protected override Employee InternalConstructEntity(EmployeeViewModel data)
      {
         var organization = GetEntityFromRepository<Partner>(userRepository, data.OrganizationId);
         return organization == null ? null : new Employee(organization);
      }

      protected override void InternalSave(Employee user, EmployeeViewModel data)
      {
         var organization = GetEntityFromRepository<Partner>(userRepository, data.OrganizationId);
         user.Organization = organization;
         userRepository.SaveOrUpdate(user);
      }

      private readonly IUserRepository userRepository;
   }

Yes, that's it. Four methods to define CRUD operations low level. Few notes:
- request.Override replaces jqGrid column names with database (NHibernate) column names;
- GetEntityFromRepository just safely converts string id to Guid and calls repository;
- some error handling code is omitted for clarity.

Behind the CRUD template methods, JqGridBaseController defines few action methods:
- Index to render page with the grid
- JsonIndex to retrieve and return data in json format;
- Delete to delete entity;
- Save to create or update entity;
- and SelectList as a bonus - to render select list options for jqGrid forms - just in case.

Nothing really fancy. I'm sure many of you can do better. But it is simple and extensible enough.
You're not forced to use JqGridController base (or anything else from this post) but it fits nicely with other parts to provide quick and easy way to build CRUD pages.

There's also another (similar) controller which is used to edit collections inside entities - for example, user's address list. It does so by taking both entity id and index in the collection as parameters. Once again it's completely transparent to the developer who only needs to provide CRUD template methods.

A lot of data driven stuff occurs in the userRepository.GetAll<Employee>(request) call. The request here comes from the jqGrid which indicates page, rows per page, sorting, filtered fields, and item to highlight.
The GetAll method takes all of this into consideration, and return a page of data, sorted, filtered, and containing the item that user wants to see. And it does this in a generic way independently of the entity type.

Now, the final piece of the puzzle is to create the view with jqGrid inside. Let's do it:

   <viewdata model="string"/>
   <set Title="'Users'" />

   <content name="bootstrap">
      <script>
         function partnerLink(cell, options, row) {
            return "<a href=\"" + options.colModel.formatoptions.baseLinkUrl + "?id="
               + row.OrganizationId + "\">" + cell + "</a>";
         }
         $(function() {
            var model = [${Html.JqGridModel<EmployeeViewModel>(null)}];
            createJqGrid("#grid", model,
               "${Url.Href<UsersController>(c => c.Delete(null))}",
               {
                  sortname: "OrganizationName",
                  url: "${Url.Href<UsersController>(c => c.JsonIndex(null, null))}",
                  editurl: "${Url.Href<UsersController>(c => c.Save(null))}"
               }, {}, "${Model}");
            $("#grid").filterToolbar();
         });
      </script>
   </content>

   <table id="grid"></table>
   <div id="pager"></div>

This is not ASP.NET WebForms but don't be scared; it's better! It's Spark View Engine and I would suggest you to give it a chance. Not that it matters for our journey.

So, here you can see our "partnerLink" function that creates a link to the partner organization. In the page load function, we use a special html helper method that takes our view model and renders jqGrid colModel array from it:

var model = [${Html.JqGridModel<EmployeeViewModel>(null)}];

Then we call a helper JavaScript function that makes most of the .jqGrid() values for us, so that we just provide grid id, colModel array, and couple of urls (load, edit, delete). But we can still override anything we need - change model before we pass it to createJqGrid, pass jqGrid options, and so on.

Notice that this method can be made even smarter if we always use JqGridBasedController - since we'll be able to pass just "Users" and it will generate CRUD urls for us. But I'm happy with the current level of DRY.

Now, it was a long journey, but only because I did introduce a bunch of concepts. The real work can be done in 10 minutes or less - define the view model, put few attributes to tweak the appearence and the behaviour, tell the controller how to link the entity and the repository, and put the grid into the view. Most of this is very mechanical and can be even copy/pasted, the only decisions one has to make is what attributes to put on the view model - which is, naturally, can't be automated since only developer (and user) can decide.

In next post(s), I'll explain the "under the hood" stuff. Meanwhile I will attach full source code - which you can't compile but can learn and use "as is".

Hope this will be useful for somebody.

1 comment:

  1. This is a nice article..
    Its very easy to understand ..
    And this article is using to learn something about it..

    c#, dot.net, php tutorial

    Thanks a lot..!

    ReplyDelete