jqGrid and ASP.NET MVC - CRUD

As promised, it's time to go ahead with a CRUD scenario for jqGrid.
Of course, we need to do some configuration first. We must define which columns are editable. For ones we will set type of edit field (through edittype parameter). We can also set some options for the edit field (editoptions) like maxlength for 'text' or dataUrl for 'select' (we will use the same actions for providing selects markup as in searching sample). Grid provides as also some validation support, which can be configured through editrules option. Be aware, that options we use in our example are only a small subset of what jqGrid allow us to configure, so I strongly advice reading the documentation.
Ok, let's initialize our grid:
<script type="text/javascript">
  $(document).ready(function() {
    $('#jqgProducts').jqGrid({
      //url from wich data should be requested
      url: '/Home/ProductsGridData/',
      //url for edit operation
      editurl: '/Home/EditProduct/',
      //type of data
      datatype: 'json',
      //url access method type
      mtype: 'GET',
      //columns names
      colNames: ['ProductID', 'ProductName', 'Supplier', 'Category', 'QuantityPerUnit', 'UnitPrice', 'UnitsInStock'],
      //columns model
      colModel: [
                  { name: 'ProductID', index: 'ProductID', align: 'left', width: 85, editable: false },
                  { name: 'ProductName', index: 'ProductName', align: 'left', width: 200, editable: true, edittype: 'text', editoptions: { maxlength: 40 }, editrules: { required: true } },
                  { name: 'Supplier', index: 'SupplierID', align: 'left', width: 225, formatter: supplierFormatter, unformat: supplierUnFormatter, editable: true, edittype: 'select', editoptions: { dataUrl: '/Home/SuppliersSelect' }, editrules: { required: true } },
                  { name: 'Category', index: 'CategoryID', align: 'left', width: 140, editable: true, edittype: 'select', editoptions: { dataUrl: '/Home/CategoriesSelect' }, editrules: { required: true } },
                  { name: 'QuantityPerUnit', index: 'QuantityPerUnit', align: 'left', editable: true, edittype: 'text', editoptions: { maxlength: 20 }, editrules: { required: true } },
                  { name: 'UnitPrice', index: 'UnitPrice', align: 'left', formatter: 'currency', formatoptions: { decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$'}, editable: true, edittype: 'text', editrules: { required: true, number: true, minValue: 0 } },
                  { name: 'UnitsInStock', index: 'UnitsInStock', align: 'left', formatter: unitsInStockFormatter, unformat: unitsInStockUnFormatter, editable: true, edittype: 'text', editrules: { required: true, integer: true, minValue: 0, maxValue: 32767} }
                ],
      //pager for grid
      pager: $('#jqgpProducts'),
      //number of rows per page
      rowNum: 10,
      //initial sorting column
      sortname: 'ProductID',
      //initial sorting direction
      sortorder: 'asc',
      //we want to display total records count
      viewrecords: true
      //grid width
      width: 'auto'
      //grid height
      height: 'auto'
    });
  });
</script>

We are going to use a navigator as placeholder for add, edit and delete buttons, but it's not necessary. The buttons can be put anywhere on page, they just need to make proper API calls, which are well described in form editing documentation section.
For complete CRUD scenario, we need to prepare three operations. We will start with easiest one, which is deleting. All we have to do is enable delete button and set url for operation (in fact the url isn't necessary, all operations can use the editurl from initial configuration, but having seperate actions for each operation is a lot cleaner).
$('#jqgProducts').navGrid(
  '#jqgpProducts',
  //enabling buttons
  { add: false, del: true, edit: false, search: false },
  //edit options
  { },
  //add options
  { },
  //delete options
  { url: '/Home/DeleteProduct/' });

Delete request contains one parameter, which is row id value. In response we can send anything we want (or just nothing). If we sent something, we will receive it in afterSubmit event. Knowing that, we can prepare delete controller action:
/// <summary>
///
Deleting selected product
/// </summary>
/// <param name="id">
product id</param>
/// <returns>
json data</returns>
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DeleteProduct(string id)
{
  //Deleting product
  bool success = _repository.DeleteProduct(Convert.ToInt32(id));

  //Returning data - we can hadle this data in form afterSubmit event
  return Json(success);
}

Let's move on to add operation. The client side part is pretty much the same (but remember, that jqGrid uses colModel options to build the form):
$('#jqgProducts').navGrid(
  '#jqgpProducts',
  //enabling buttons
  { add: true, del: true, edit: false, search: false },
  //edit options
  { },
  //add options
  { width: 'auto', url: '/Home/AddProduct/' },
  //delete options
  { url: '/Home/DeleteProduct/' });

On server side we receive a parameter for every editable field. We just need to make an object out of this and add it to repository:
/// <summary>
///
Adding product
/// </summary>
/// <param name="postData">
postData collection</param>
/// <returns>
json data</returns>
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult AddProduct(FormCollection postData)
{
  //Creating new product object based on postData
  Product product = new Product();
  product.ProductName = postData["ProductName"];
  product.SupplierID = Convert.ToInt32(postData["Supplier"]);
  product.CategoryID = Convert.ToInt32(postData["Category"]);
  product.QuantityPerUnit = postData["QuantityPerUnit"];
  product.UnitPrice = Convert.ToDecimal(postData["UnitPrice"].Replace(".", CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator));
  product.UnitsInStock = Convert.ToInt16(postData["UnitsInStock"]);
  product.Discontinued = false;

  //Adding product to repository
  bool success = _repository.AddProduct(product);

  //Returning data - we can hadle this data in form afterSubmit event
  return Json(success);
}

Last operation to implement is editing. There are two edit approaches to consider (actually there are three, but I'm going to skip cell editing): inline editing and form editing. In inline editing we are adding onSelectRow event handler in which we call editRow (for displaying edit controls) or saveRow (for posting new values to server) method:
$('#jqgProducts').setGridParam({
  onSelectRow: function(id){
    if(id && id != lastSel){
      //save changes in row
      $('#jqgProducts').saveRow(lastSel, false);
      lastSel=id;
    }
    //trigger inline edit for row
    $('#jqgProducts').editRow(id, true);
  }
});

Form editing works the same as adding. We just enable the button and then grid constructs the form based on colModel:
$('#jqgProducts').navGrid(
  '#jqgpProducts',
  //enabling buttons
  { add: true, del: true, edit: true, search: false },
  //edit options
  { width: 'auto' },
  //add options
  { width: 'auto', url: '/Home/AddProduct/' },
  //delete options
  { url: '/Home/DeleteProduct/' });

No matter which approach we choose, request looks the same. We are getting row id and values for all the editable fields. What we need to do is get object from repository and set new values:
/// <summary>
///
Editing product
/// </summary>
/// <param name="postData">
postData collection</param>
/// <returns>
json data</returns>
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditProduct(FormCollection postData)
{
  //Editing product based on postData
  Product product = _repository.GetProduct(Convert.ToInt32(postData["id"]));
  product.ProductName = postData["ProductName"];
  product.SupplierID = Convert.ToInt32(postData["Supplier"]);
  product.CategoryID = Convert.ToInt32(postData["Category"]);
  product.QuantityPerUnit = postData["QuantityPerUnit"];
  product.UnitPrice = Convert.ToDecimal(postData["UnitPrice"].Replace(".", CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator));
  product.UnitsInStock = Convert.ToInt16(postData["UnitsInStock"]);

  //Sending changes back to repository
  bool success = true;
  try
  {
    _repository.SubmitChanges();
  }
  catch (Exception ex)
  {
    Debug.Write(ex.Message);
    success = false;
  }

  //Returning data - we can hadle this data in form afterSubmit event
  return Json(success);
}

That makes our CRUD scenario complete. Source code can be downloaded from usual location. To make deleting easy, I have changed FK_Order_Details_Products foreing key in Northwind database.

7 comments:

Anonymous said...

I have a problem with the editing. When I edit a row with a select filled from a partial view like the Supplier and Catagory columns in your example, the correct default value is only chosen the first time I edit a row. Every row I edit after that has the previous default value in the select.
Have you encountered this problem or better yet do you know how to solve it?

Thanks.

Tomasz Pęczek said...

Unfortunately I have never encounter thi sproblem, but I will be happy to look through your code (if you can post it or mail it) and help you find a solution.

Tahir Hassan said...

I have just downloaded Tortoise SVN to copy the example project you are providing. Although ideally all we should need is an svn client to see another developer's source code, in this case perhaps it would be better to provider a zip. Even though it seems like a small project it is taking over ten minutes just to download this project's directory (I am only copying the directory jqGridExample).

But thank you for providing this resource :)

Tomasz Pęczek said...

All complete samples can be downloaded zip's in separate releases from here: http://tpeczek.codeplex.com/

Tahir Hassan said...

Hi Tomasz

Thank you for your code sample, it is an amazing piece of work and it is the only decent sample code I can find for using MVC with JqGrid.

I will recommend your website to colleagues.

Tahir

Bhoomi said...

Have u tried using autocomplete in form editing of the grid?
If yes could you please share your code.

Thanks,
Bhoomi.

Tomasz Pęczek said...

No I haven't, but I can try to prepare such a sample - but I'm affraid it make take a couple days, as I have a very little of free time lately.