Wednesday, September 9, 2009

ASP.NET MVC with jQuery Grid

Incorporating the jQuery Grid With ASP.NET MVC

Scenario: You have an ASP.NET MVC application and don't want to custom build smart controls like the ASP.NET GridView, ListView, etc, that support sorting and paging, as well as filtering and searching for data, and all of this using Ajax.

Solution: The jQuery Grid plug-in. What tools/plug-ins do I need?

  • jQuery version 1.3 and up
  • jqGrid version 3 and up (this post references version 3.4.4)
  • A strong cup of coffee

In the past you had to also add the jQuery modal and table row drag-and-drop plug-ins, but now the jqGrid team has made it easy and you can select to roll those into your download.

So where do I even start? Well, I am glad you asked.


Before we just jump in and start with the steps needed, let me just briefly summarize what you will need to do:

    Download the jQuery JavaScript files and plug-ins
  1. Create your ASP.NET MVC project in Visual Studio 2008 and add the three controller actions, one to render the account view, another to get the Ajax request's list of contacts, and lastly to support the update and delete operations of the contact.
  2. Create your own jQuery file that will contain your contact's jQuery Grid.
  3. Define the HTML needed to render the jqGrid (2 lines of HTML only!)

    1. Download the plug-in.

    It may seem obvious to some, but start downloading your plug-ins from these locations and save them to your hard-drive:

    jQuery

    http://docs.jquery.com/Release:jQuery_1.3.2

    jqGrid

    http://www.trirand.com/blog/?page_id=6 (select the models from Base and Editing only)

    2. Now create a standard ASP.NET MVC project.

    In short the jqGrid will make Ajax calls to your MCV controller actions. The application now has a HomeController and should have at least an Index action. In this article we will be demoing how to display data, so the Index will suffice to render our page, but we will be creating another action that will be responsible for rendering the content we need for our grid-list Ajax call later.

    What we will be showing in this demo is an Account's page that has several tabs on it. One of the tabs will contain the list of all contacts for this account. In stead of just showing a list of someting, in this example you will be exposed to filtering a list, in this case by account, and showing how this can be done with the jqGrid (look out for the "postData" property of the jqGrid definition later in this post).

    3. Add CSS and JavaScript files to project.

    Before we start building the app, let’s find a place to store our CSS and jQuery files/plug-ins in our ASP.NET MVC project. For this I like to create a folder under the UI (my MVC project folder's name):

    a. Content - contains all the CSS content, my own custom files as well as the jQuery Grid's and possibly other plug-in CSS files. This will also have an Images sub-folder that the CSS files’ image paths will need to point to, so you may have to do some find-replace-all to change the image paths in the CSS files to whatever your folder structure is.

    b. Scripts - All my own and the jQuery, jqGrid and possibly other JavaScript files.

    Solution Explorer Folder Structure

    In this example there are a large amount of jQuery custom files for a CRM application, hence the CRM folder. Wherever minification of the scripts was important, a Min sub-folder was created that would be rendered in a production and/or QA environment where the requirement to debug scripts was not necessary. The jQuery Base folder contains all jQuery files as well as sub-folders for all jQuery plug-ins called Plugins. When the plug-in itself may contain more than one js file, like in this example the jqGrid, a separate sub-folder was created under the Plugins folder with the plug-in’s name, like jQuery.grid. Of course you may not want to create a folder structure with this level of detail, and is completely up to you, none of the plug-ins have any requirement either way.


    4. Hook-up your files to the HTML page.

    Now you need to let your web application's Index page know that it needs to load your scripts for the jQuery and jqGrid. There are other, and arguably better ways to do this, especially if you have a large number of plug-ins, but for now simply load them like this in your HTML header:

    FYIThe first entry is a link to the CSS file. The jqGrid you downloaded came with a CSS file, you need to create a link to it. The second are the two js files, be sure to specify the jQuery-1.3.2 file before the jqGrid file as the jqGrid is dependent on the jQuery-1.3.2 file (JavaScript files are downloaded in sequence by our browser engine).

    5. Define the jqGrid.

    Now you are ready to define your jqGrid; provide a JSON object that will let the jqGrid know what columns to show, should it support paging, sorting, what is the URL to get the data and what is the URL to POST data back, and many other properties. Place this definition in your header, but since I have about a few of these grids and did not want to clutter the HTML header I created a separate js file that I link to.


    CRMScriptingAccountFileLocation

    We can get fancy here with drop downs, loading drop down dynamically, custom formatting and all kinds of built-in validations. We will not be getting into this in this post. Remember this is in its own js file, so no need for script tags:


    SetupJqGridListOfAccountChildrenCall

    Before we see what the definition of the grid looks like, we decided that we would group all the jqGrids into one function, SetupJqGridListOfAccountChildren. This function contains a call to twelve other functions, each one responsible for the definition of its own grid. One of these is our very own list of contact’s grid. You of course do not have to do this, especially if you only have one grid to display, in this demo we display twelve grids on one page! I kow…..keep it to yourselves.

    One thing you may take away from this is that when you define the properties of the grid there is some repetition and passing in the parms shown here may (or not) make your maintenance easier, especially if you cal the same grid in different situations, with different URLs (list contacts with a different controller and action for example), no need to change the hard-coded properties in the gird or create two very similar or identical grids, just pass in a different URL, etc.

    function SetupJqGridListOfAccountChildren()

    Now lets see what the actual jQuery Grid definition looks like. Note you may decide to use an XML formatted file, in this demo we went with a JSON formatted object. See the jqGrid documentation for a sample of this.

    function jqGridAccountContact(item, listURL, editURL, $rows, hideGrid) {

    var lastsel;

    var ignoreAlert = false;

    $(item)

    .jqGrid({

    url: listURL,

    postData: { accountId: $('fieldset#AccountDetails #Id').val() },

    datatype: "json",

    colNames: ['Id', 'First Name', 'Last Name', 'Phone', 'Title'],

    colModel: [

    { name: 'Id', index: 'Id', hidden: true, hidedlg: true },

    { name: 'FirstName', index: 'FirstName', width: 40 },

    { name: 'LastName', index: 'LastName', width: 40 },

    { name: 'PhoneNumber', index: 'PhoneNumber', width: 80 },

    { name: 'Title', index: 'Title', width: 80 }

    ],

    jsonReader: {

    root: "List",

    page: "CurrentPage",

    total: "TotalPages",

    records: "TotalRecords",

    userdata: "Errors",

    repeatitems: false,

    id: "0"

    },

    rowNum: $rows,

    rowList: [5, 10, 20, 50, 200],

    imgpath: jQueryImageRoot,

    pager: $(item + 'Pager'),

    recordtext: 'Contact(s)',

    sortname: 'Id',

    viewrecords: true,

    sortorder: "asc",

    altRows: true,

    loadComplete: function() {

    $(item).setColProp('EditUrl', { editoptions: { title: 'Edit this record', align: 'right', color: 'red'} });

    $(item + ' tr.jqgrow td:contains("Decommissioned")').css({ 'color': '#FF0000' })

    .parent().css({ 'background-color': '#EFEFEF' });

    },

    loadError: function(xhr, st, err) { if (!ignoreAlert) { alert(ajaxErrorMessage); } },

    loadBeforeSend: function(xhr) {

    if ($('#snames').val() == 'Id')

    if (isNaN($('#sval').val())) {

    alert('Sorry! the value for Id must be numeric');

    ignoreAlert = true;

    return false;

    }

    },

    search: true,

    searchdata: {},

    caption: "List of all Contacts",

    editurl: editURL + '?accountId=' + $('fieldset#AccountDetails #Id').val() + '&rows=' + $rows + '&page=1',

    hiddengrid: hideGrid,

    height: 215,

    width: 724

    })

    .navGrid(item + 'Pager',

    { refresh: true, edit: true, add: true, del: true, search: false }, //options

    {height: 280, width: 500,

    reloadAfterSubmit: true,

    modal: true,

    processData: 'Updating the Contact info...',

    editCaption: 'Edit a Contact',

    afterSubmit: function(response, postdata) {

    return SetupResponseMessage(item, response, "The Contact has been successfully updated.")

    }

    }, // edit options

    {height: 280, width: 500,

    reloadAfterSubmit: true, //need the new generated id

    modal: true,

    processData: 'Adding the new Contact...',

    addCaption: 'Add a Contact',

    afterSubmit: function(response, postdata) {

    return SetupResponseMessage(item, response, "The Contact has been successfully created.")

    }

    }, // add options

    {height: 120, width: 320,

    reloadAfterSubmit: true,

    afterShowForm: function(item) {

    $('#eData', item).focus();

    },

    afterSubmit: function(response, postdata) {

    var errors = eval('(' + response.responseText + ')').Errors.Errors;

    if (errors) {

    $('#FormError>td', '#TblGrid_' + item).text(errors);

    $('#FormError>td>ul', '#TblGrid_' + item)

    .css({ 'list-style-type': 'disc', 'list-style-position': 'outside' });

    $('#FormError', '#TblGrid_' + item).show();

    return [false, errors];

    }

    return true;

    }

    }, // del options

    {} // search options

    )

    .navButtonAdd(item + 'Pager', {

    caption: "",

    title: "Select Columns",

    buttonimg: jQueryImageRoot + '/column_Select.gif',

    position: 'last',

    onClickButton: function() { $(item).CRM_SetColumns(); }

    })

    ;

    }

    This looks scary, I know, but once you have a template you can copy and re-use it. You can also with jQuery extensions define it once, simply extend it as needed for customization. For example, we have dozens of lookup tables that we map using the jqGrid, we only have ONE, and simply change the URL as needed.

    Let’s break this down somewhat. There are many more properties you can specify to tune your columns’ properties, like alignment, etc, but for brevity will not be getting into it.

    · The function parameters:

    The function parameters

    · The URL property:

    The URL property

    · The postData:

    The postData

    Define any additional data you want to post back. In this example since I do not want to or can hardcode the account’s ID as part of the URL, I use jQuery to find it and add it to the postData. This is critical otherwise the MVC action will fail since it is expecting an accounted value.

    · The dataType:

    datatype: "json",

    The data type of the returned object, in this example it will be a JOSN serialized object, could be XML also. As a side note you could generate the static HTML table server-side if you waned to and have an option to point the jqGrid to it and all of the available paging and sorting, etc, will come out of the box, however, this requires that ALL data be loaded down to the client; if you have 500 contacts, all 500 will need to be specified in the HTML. I never use this option.

    · The colNames:

    colNames: ['Id', 'First Name', 'Last Name', 'Phone', 'Title'],]

    Simply a list of all the header labels. The number of header labels MUST match the number of properties you specify for “colModel”, otherwise jqGrid will not be able to render your grid and will throw a run-time alert. These names do not have to match the names you want to use for sorting and searching that is what the “name” and “index” properties are for in the “colModel” definition. See the jqGrid documentation for a sample of this.

    · The colModel:

    colModel: [

    This where you define the CSS, validation and operations properties for each column. The available set of properties is enormous. Only a few are shown here, only name and index need to be specified, you could ignore the rest and rely on defaults. In case you were wondering, yes you can protect fields, hide some and show them only when editing the record, create SELECT tags with static or dynamically (loaded from the database) lists, validate input, etc, etc, etc. No need to worry just about everything imaginable is supported with callback functions to create your own. It is the index property that is used when sorting data, so make sure it is right and can be recognized by the server. See the jqGrid documentation for a sample of this.

    · Jsonreader

    jsonReader: {}

    Optional, and can use it if you want to override the default JOSN properties for the JSON serialized DTO that will be passed back to the grid. In this example the property that will contain the actual list of data is called “root”. The “page”, “total” and “records” are used to support paging. Again entirely optional, but if not specified you need to make sure your DTO property names conform to the jqGrid definitions.

    · Overriding default properties (again optional):

    rowNum: $rows, rowList: [5,10,20,50,200],

    · All optional, but are very useful:

    1. rowNum: How many rows to display by default (one of the parms to the function)

    2. rowList: The tag.

    4. pager: Important, the HTML tag ID of the

    that contains the paging controls. Not sure why this was done this way, but as you will see later, you need to specify where the grid needs to be rendered in your HTML.

    5. recordtext: The text to display next to the counter for the number of rows that exist (not the number of rows retrieved or loaded into the grid!)

    6. sortname: The default sort column

    7. sortorder: “asc” well ascending of course J

    8. altRows: highlight alternate rows

    · Event loadComplete:

    loadComplete: function()

    One of many, this event fires once all the required data for the grid has been downloaded, before the grid is being rendered. In this example, although the column “EditUrl” does not exist shows a way some CSS can be passed into the jqGrid to change its color to red, align it right and give it a title. It also looks for any row that has “Decommissioned” and makes it red. It lastly also changes that text’s background color too. The possibilities are endless; you can pass in CSS or ATTR properties as key-value pairs as part of the “editOptions” property.

    · Paging, Sorting, editing….

    .navGrid()

    Included this, that although completely optional is important if you want to enable/disable certain operations. Like for example here the grid will allow the user to refresh the list, edit/update a contact, add a new contact, delete one and even search for a contact. The code definitions that follow in the code sample above can get a bit involved and not necessary to discuss here at this time, but is basically used to define how to perform the CRUD operations.

    6. Now let’s define the HTML required to get this working.

    " height="52" width="717">

    That’s right, two lines of HTML! The first to specify the table name of the grid and the second the

    container for the paging and where the other CRUD icons will appear. Remember the “item” parm in the “jqGridAccountContact” function? Well that’s the name for the “item” parm: “ListAccountContacts”. I prefer the naming convention for the pager to simply append Pager to the table name. Don’t ask why the jqGrid needs this, I honestly do not know. Well, that is all the HTML you will ever have to write. HA, that’s great you say, well which brings us to the next step, which is to hook up all this fancy stuff to the server that has to retrieve the data, perform a search if requested, convert the data to a serializable JSON object, and if requested also perform delete/update operations. To support delete/update operations we define a different MVC action to keep it clean (SRP).

    7. Define the action to render the Account’s page.

    public ActionResult Edit(int accountId)

    8. Define the action to retrieve the list of contacts:

    public JsonResult ListAllContactsForAccount(int accountId, int page, int rows, string sord, string sidx,

    string search, string searchField, string searchOper,

    string searchString)

    {

    JsonListDTO<ContactDTO> dto;

    switch (sidx)

    {

    case "Id":

    sidx = "l.id";

    break;

    case "FullNameUrl":

    sidx = "FirstName";

    break;

    case "ActivityStatus":

    sidx = "l.ActivityStatus";

    break;

    case "Title":

    sidx = "l.Title";

    break;

    }

    #region Validate input data

    IAccount account = Repository<IAccount>.Get(accountId);

    if (account == null)

    throw new ArgumentException("The account for which data has been requested does not exist.");

    #endregion

    #region If all validation passed, get the data and return Json data

    //Reset current page to 1 if the rows to display per page has changed

    if (TempData["rowsToDisplay"] != null && (int) TempData["rowsToDisplay"] != rows)

    page = 1;

    //Get all the contacts for this

    IEnumerable<ContactDTO> contacts = _contactRepository.GetAllAccountContacts(account, sord, sidx, rows, page);

    dto = SetupJsonDTOList(page, contacts, _contactRepository.NoOfRecordsRetrieved.GetValueOrDefault(),

    CalculateTotalNoOfPages(_contactRepository.NoOfRecordsRetrieved.GetValueOrDefault(),

    rows),

    _contactRepository.Message);

    TempData["rowsToDisplay"] = rows;

    return Json(dto);

    #endregion

    }

    This may seem like a lot, but it is quite remedial. The first thing you want to do it reformatted the “sidx”, the column to sort by if needed. Since we use NHibernate maps, the DAO that fetches the data used an alias called “l”, short for list, hence the “l.” notation. So for example if “sidx” is “Title” it is converted to “l.Title”, and in my NHibernate DAO’s HQL query then simply maps the title to the Contact alias “l”, thus NHibernate generates

    select l.FirstName, l.LastName, l.Title…. from Contact as l where l.AccountId = 1

    Notice the “JsonListDTO” generic object definition. You may select to use an anonymous definition; in this example the type of DTO that may be serialized must be specified.

    Then get the account: IAccount account = Repository<IAccount>.Get(accountId);

    If it does not exist, throw an exception.

    Note the method returns a JsonReult, using the MVC built-in Json conversion to create the JSON serialized object.

    Why a DTO? Well I am glad you asked. As with many examples, they are overly simplified and “haacked” together. The “Contact” entity has an “Account” property that because of recursion issues in the Json conversion cannot be serialized and converted directly (the account has list of contacts and contact has an account property). So in stead use NHibernate to refactor the list of contacts into a “dumb” DTO, that returns a DTO properties without any complex getters, just strings in this example, very light-weight, only retrieve what you need. You can create overloaded constructors for more scenarios.

    using System;

    [Serializable]

    public class ContactDTO

    {

    public ContactDTO(int id, string firstName, string lastName, string title, string phoneNumber, int totalRows)

    {

    Id = id;

    FirstName = firstName;

    LastName = lastName;

    Title = title;

    PhoneNumber = phoneNumber;

    TotalRows = totalRows;

    }

    public virtual int Id { get; set; }

    public virtual string FirstName { get; set; }

    public virtual string LastName { get; set; }

    public virtual string Title { get; set; }

    public virtual string PhoneNumber { get; set; }

    public int TotalRows { get; set; }

    }

    An important note you is that the property names MUST match those of the “colModel” “name” and index” jqGrid properties.

    jqGrid HTML View

    As you can see form the example above, I have one HTML page that is very busy indeed. So busy I had to cut off some of the tabs. Behind most tabs is a jQuery grid. In one particular instance it has 5 grids. They perform very well.

    Couple of pointers:

    • · The difference between traversing the object tree to get data, especially when using LINQ is not very practical in a surprising high number of instances. During the project the grid loaded very slowly. Once the LINQ queries were replaced by DTOs and light-weight HQL or ICriteria NHibernate queries, the performance increased exponentially.

    • · To create the update/delete functionality takes more server-side code, none more required on the client-side. All that is necessary for the jqGrid to work on the client has already been completed. I recommend creating another action on the HomeController that receives the parms that support CRUD operations fully, like the “oper” parm that is posted back together with the “Id” and other contact data. The “oper” parm needs to be evaluated by the server to determine what operation is requested. Then load the contact with the new data and save it to the database, and you’re done:

    if (String.Compare(oper, "Edit", true) != 0)

    • · To render server-side validation messages in the modal dialog (if that is the option you selected for CRUD, there are inline and cell-edit options as well), create the list of errors in your action and place them in the DTO’s “Errors” property (see jsonReader’s userdata properties).


    I sincerely hope that this has been helpful and will get you started with the jQuery Grid quickly. Please let me know what you thought of this post and if you would like to see more specific examples. Any feedback and comments will be appreciated and will help us all to share pertinent and useful information; no need to re-invent the wheel all over again :-)

No comments:

Post a Comment