In one of comments to my jqGrid strongly typed helper post I was asked for form editing demo with file upload and TinyMCE integration, so here it comes.
The demo will provide a CRUD functionality for Employees table from Northwind database. I will skip Employee and EmployeesRepository classes here (full source code for entire sample can be downloaded here) and go straight to ViewModel which will be used by the helper:
public class EmployeeViewModel
{
#region Properties
[ScaffoldColumn(false)]
public int Id { get; set; }

[JqGridColumnSortable(false)]
[JqGridColumnFormatter("$.photoFormatter", UnFormatter = "$.photoUnFormatter")]
[JqGridColumnLayout(JqGridAlignments.Center, Width = 35)]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.File)]
public string Photo { get; private set; }

[StringLength(25)]
[JqGridColumnSortable(true)]
[DisplayName("Title of courtesy")]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.Text)]
public string TitleOfCourtesy { get; set; }

[Required]
[StringLength(10)]
[DisplayName("First name")]
[JqGridColumnSortable(true)]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.Text)]
public string FirstName { get; set; }

[Required]
[StringLength(20)]
[DisplayName("Last name")]
[JqGridColumnSortable(true)]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.Text)]
public string LastName { get; set; }

[StringLength(30)]
[JqGridColumnSortable(true)]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.Text)]
public string Title { get; set; }

[AllowHtml]
[JqGridColumnSortable(false)]
[JqGridColumnLayout(JqGridAlignments.Left, Width = 450)]
[JqGridColumnEditable(true, EditType = JqGridColumnEditTypes.TextArea)]
public string Notes { get; set; }
#endregion

#region Constructors
public EmployeeViewModel()
{ }

public EmployeeViewModel(Employee employee)
{
this.Id = employee.Id;
this.TitleOfCourtesy = employee.TitleOfCourtesy;
this.FirstName = employee.FirstName;
this.LastName = employee.LastName;
this.Title = employee.Title;
this.Notes = employee.Notes;

RouteValueDictionary photoUrlRouteData = new RouteValueDictionary();
photoUrlRouteData["controller"] = "Home";
photoUrlRouteData["action"] = "EmployeePhoto";
photoUrlRouteData["id"] = employee.Id;
Photo = RouteTable.Routes.GetVirtualPathForArea(HttpContext.Current != null ? HttpContext.Current.Request.RequestContext : null, null, photoUrlRouteData).VirtualPath;
}
#endregion
}
That's a lot of attributes, but we will focus only on two properties - Photo for which we want file upload functionality and Notes which we want to integrate with TinyMCE. We will start with Notes. Two most important attributes here are AllowHtml (so TinyMCE can post some HTML for this property) and JqGridColumnEditable (we want jqGrid to generate a TextArea for us, so we can attach TinyMCE to it). The only problem with TinyMCE here is that it can't be attached to an element which is not visible. Because of that we have to recreate it every time the form is displayed. There are two events in jqGrid navigator action options, which will allow us to do it easily: afterShowForm and onClose. Here goes the JavaScript functions for those events:
$.onAfterShowForm = function(formSelector) {
//Add TinyMCE editor instance
$('textarea', formSelector).attr('cols', '50').attr('rows', '15').tinymce({
theme : 'advanced',
theme_advanced_toolbar_location : 'top'
});
};

$.onClose = function(formSelector) {
//Remove TinyMCE editor instance
$('textarea', formSelector).tinymce().remove();
};
So TinyMCE is working, what about the file upload? We can set the edittype to file and than proper input element will be created by jqGrid, but this is where the support ends - jqGrid will not post the file for us. Tony Tomov suggests creating an additional submit button for this, but this way you need to post data in two steps. I really wanted to have all data posted at once, so I decided to go a little bit "hacky" here. Please be aware, that following solution will completely replace jqGrid built in post functionality - if you need any part of it, you need to write it yourself here. I'm going to use Ajax File Uploader plugin, to post entire form at once. To achieve this I will change some of the form attributes in onInitializeForm event:
//WARNING: This will completely replace build in jqGrid post behavior
$.onInitializeForm = function(formSelector) {
//Set enctype='multipart/form-data'
$(formSelector).attr('enctype', 'multipart/form-data')
//method='POST'
.attr('method', 'POST')
//remove onsubmit
.removeAttr('onsubmit')
//attach Ajax File Uploader plugin
.iframePostForm({
//When post request is completed
complete: function(response) {
//check if operation was successful
if(response != 'success') {
//if not, then display error message
$('#FormError>td', formSelector).html(response);
$('#FormError', formSelector).show();
} else {
var id = $('#id_g', formSelector).val();
//if yes, reload jqGrid
$('#employees').trigger('reloadGrid');
//and restore current selection
setTimeout(function(){
$('#employees').jqGrid('setSelection', id);
}, 1000);
}
//Remove class from submit button
$('#iframe_submit').removeClass('ui-state-active');
}
});
//Create inputs for 'id' and 'oper' keys which jqGrid adds to request by default
$('#id_g', formSelector).after($('<input />').attr('id', 'iframe_id').attr('type', 'text').attr('name', 'id'));
$('#id_g', formSelector).after($('<input />').attr('id', 'iframe_oper').attr('type', 'text').attr('name', 'oper'));
//Change the submit button id, so jqGrid will not attach its click event
$('#sData').attr('id', 'iframe_submit')
//and attach our click event
.click(function(e) {
//When user clicks the submit button hide error message
$('#FormError', formSelector).hide();
var id = $('#id_g', formSelector).val();
//set values for 'id' and 'oper' inputs
$('#iframe_id', formSelector).val(id);
$('#iframe_oper', formSelector).val((id == '_empty') ? 'add' : 'edit');
//set action url based on operation type
$(formSelector).attr('action', (id == '_empty') ? '@Url.Action("InsertEmployee")' : '@Url.Action("UpdateEmployee")')
//add class to submit button
$('#iframe_submit').addClass('ui-state-active');
//and submit form
$(formSelector).submit();
return false;
});
};
Now we can create the view. First we should instantiate jqGrid helper with all the desired options:
@{
ViewBag.Title = "jqGrid in ASP.NET MVC - Employees [Form Editing]";

var grid = new Lib.Web.Mvc.JQuery.JqGrid.JqGridHelper<jqGrid.DungDepTrai.Models.EmployeeViewModel>("employees",
dataType: Lib.Web.Mvc.JQuery.JqGrid.JqGridDataTypes.Json,
methodType: Lib.Web.Mvc.JQuery.JqGrid.JqGridMethodTypes.Post,
pager: true,
rowsNumber: 10,
sortingName: "LastName",
sortingOrder: Lib.Web.Mvc.JQuery.JqGrid.JqGridSortingOrders.Asc,
url: Url.Action("FindEmployees"),
width: 1077,
viewRecords: true
).Navigator(new Lib.Web.Mvc.JQuery.JqGrid.JqGridNavigatorOptions() { Search = false, View = false },
editActionOptions: new Lib.Web.Mvc.JQuery.JqGrid.JqGridNavigatorActionOptions() { Url = Url.Action("UpdateEmployee"), OnInitializeForm = "$.onInitializeForm", OnAfterShowForm = "$.onAfterShowForm", OnClose = "$.onClose" },
addActionOptions: new Lib.Web.Mvc.JQuery.JqGrid.JqGridNavigatorActionOptions() { Url = Url.Action("InsertEmployee"), OnInitializeForm = "$.onInitializeForm", OnAfterShowForm = "$.onAfterShowForm", OnClose = "$.onClose" },
deleteActionOptions: new Lib.Web.Mvc.JQuery.JqGrid.JqGridNavigatorActionOptions() { Url = Url.Action("DeleteEmployee") }
);
}
Then we can get required HTML out of it:
@grid.GetHtml()
And add the JavaScript:
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.jqGrid.locale-en-3.8.2.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.jqGrid-3.8.2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.iframe-post-form.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/TinyMCE/tiny_mce.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/TinyMCE/jquery.tinymce.js")" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function () {
@grid.GetJavaScript()
});

//Every photo have constant URL, this formatter will make them refresh at every reload <-- only for presentation purposes
$.photoFormatter = function(cellvalue, options, rowObject) {
return "<img style='width:25px' src='" + cellvalue + "&rand=" + (Math.random() * 10000000) + "' />";
};

//Always unformat photo as empty value
$.photoUnFormatter = function(cellvalue, options, cellobject) {
return '';
};

//WARNING: This will completely replace build in jqGrid post behavior
$.onInitializeForm = function(formSelector) {
...
};

$.onAfterShowForm = function(formSelector) {
...
};

$.onClose = function(formSelector) {
...
};
</script>
The only thing that is missing here is some server side code for handling the requests:
public ViewResult Employees()
{
return View();
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult FindEmployees(JqGridRequest request)
{
int totalRecordsCount = _employeesRepository.GetCount();

JqGridResponse response = new JqGridResponse()
{
TotalPagesCount = (int)Math.Ceiling((float)totalRecordsCount / (float)request.RecordsCount),
PageIndex = request.PageIndex,
TotalRecordsCount = totalRecordsCount
};
response.Records.AddRange(from employee in _employeesRepository.FindRange(String.Format("{0} {1}", request.SortingName, request.SortingOrder), request.PageIndex * request.RecordsCount, request.RecordsCount)
select new JqGridRecord<EmployeeViewModel>(Convert.ToString(employee.Id), new EmployeeViewModel(employee)));

return new JqGridJsonResult() { Data = response };
}

[NoCache]
public ActionResult EmployeePhoto(int id)
{
//We should keep images in file system
string employeePhotoPath = GetEmployeePhotoPath(id);
if (!System.IO.File.Exists(employeePhotoPath))
{
Employee employee = _employeesRepository.FindByKey(id);
if (employee.Photo != null)
{
using (FileStream employeePhotoStream = new FileStream(employeePhotoPath, FileMode.Create, FileAccess.Write))
//Skip Ole bytes array
employeePhotoStream.Write(employee.Photo, 78, employee.Photo.Length - 78);
}
else
//You might want to return some default image here
return new EmptyResult();
}
return File(employeePhotoPath, "image/bmp");
}

[AcceptVerbs(HttpVerbs.Post)]
public ContentResult InsertEmployee([Bind(Exclude = "Id")]EmployeeViewModel viewModel, HttpPostedFileBase photo)
{
if (ModelState.IsValid)
{
try
{
if (photo != null && photo.ContentLength > 0 && photo.ContentType != "image/bmp")
return Content("Photo must be in BMP format.");
else
{
Employee employee = new Employee();
employee.LastName = viewModel.LastName;
employee.Notes = viewModel.Notes;
employee.Title = viewModel.Title;
employee.TitleOfCourtesy = viewModel.TitleOfCourtesy;
employee.FirstName = viewModel.FirstName;
_employeesRepository.Add(ref employee);

_employeesRepository.SaveChanges();

photo.SaveAs(GetEmployeePhotoPath(employee.Id));
}
}
catch
{
//There should be some decent exception handling here
return Content("An error occurred during employee insert.");
}

return Content("success");
}
else
return Content("Invalid employee data.");
}

[AcceptVerbs(HttpVerbs.Post)]
public ContentResult UpdateEmployee(EmployeeViewModel viewModel, HttpPostedFileBase photo)
{
if (ModelState.IsValid)
{
try
{
Employee employee = _employeesRepository.FindByKey(viewModel.Id);
if (employee != null)
{
if (photo != null && photo.ContentLength > 0)
{
if (photo.ContentType == "image/bmp")
photo.SaveAs(GetEmployeePhotoPath(viewModel.Id));
else
return Content("Photo must be in BMP format.");
}

employee.LastName = viewModel.LastName;
employee.Notes = viewModel.Notes;
employee.Title = viewModel.Title;
employee.TitleOfCourtesy = viewModel.TitleOfCourtesy;
employee.FirstName = viewModel.FirstName;

_employeesRepository.SaveChanges();
}
else
return Content("Couldn't found employee for update.");
}
catch
{
//There should be some decent exception handling here
return Content("An error occurred during employee update.");
}

return Content("success");
}
else
return Content("Invalid employee data.");
}

[AcceptVerbs(HttpVerbs.Post)]
public HttpStatusCodeResult DeleteEmployee(int id)
{
try
{
_employeesRepository.Delete(id);
_employeesRepository.SaveChanges();

return new HttpStatusCodeResult(200, "Succeeded");
}
catch
{
//There should be some decent exception handling here
return new HttpStatusCodeResult(500, "An error occurred during employee delete.");
}
}
Now we can play with the demo.
I hope you will find it helpful.

I've been using jqGrid a lot over last year. During this time I have created many helper classes for it. Recently I decided to put all those classes together, clean up, rewrite and publish for the community.
I made helper available as part of my Lib.Web.Mvc library, and you can download it here (source code included), or go for the NuGet package.
Now let me show you simple usage example. We will need a model class for our grid:
public class ProductViewModel
{
#region Properties
public int Id { get; set; }

public string Name { get; set; }

[JqGridColumnSortingName("SupplierId")]
public string Supplier { get; set; }

[JqGridColumnSortingName("CategoryId")]
public string Category { get; set; }

[DisplayName("Quantity Per Unit")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public string QuantityPerUnit { get; set; }

[DisplayName("Unit Price")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public decimal? UnitPrice { get; set; }

[DisplayName("Units In Stock")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public short? UnitsInStock { get; set; }
#endregion

#region Constructor
public ProductViewModel()
{ }

public ProductViewModel(Product product)
{
this.Id = product.Id;
this.Name = product.Name;
this.Supplier = product.Supplier.Name;
this.Category = product.Category.Name;
this.QuantityPerUnit = product.QuantityPerUnit;
this.UnitPrice = product.UnitPrice;
this.UnitsInStock = product.UnitsInStock;
}
#endregion
}
Following standard DataAnnotations attributes are supported:
  • DisplayName - This will set the name for the column (default is property name).
  • DisplayFormat - Value set for NullDisplayText will be used as cell value if property value will be null. Value set for DataFormatString will be used for formatting property value (on server side).
  • HiddenInput - This will set JqGridColumnModel.Hidden to true.
  • Range - Value set for Maximum will be used for EditRules.MaxValue of editable column and value set Minimum for will be used for EditRules.MinValue.
  • Required - This will set EditRules.Required to true for editable column.
  • ScaffoldColumn - If set Scaffold to false, there will be no column created for property with this attribute.
  • StringLength - Value set for MaximumLength will be used for EditOptions.MaximumLength of editable column.
Additionaly the helper support those custom attributes (from Lib.Web.Mvc.JQuery.JqGrid.DataAnnotations namespace):
  • JqGridColumnLayout - This will set the layout attributes for the column (alignment, initial width etc.).
  • JqGridColumnSortable - This will set the sorting options for the column (by default every column is sortable with property name as sorting name).
  • JqGridColumnFormatter - This will set the predefined (with options) or custom formatter for the column.
  • JqGridColumnEditable - This will set the edit options for the column.
  • JqGridColumnSearchable - This will set the search options for the column.
When our model class is done, we can start creating the view. First we need to include some CSS and JavaScript files:
<link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/jqGrid/jquery-ui-jqgrid.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.jqGrid.locale-en-3.8.2.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.jqGrid-3.8.2.min.js")" type="text/javascript"></script>
Then we can instantiate the helper and set desired options:
@{
var grid = new Lib.Web.Mvc.JQuery.JqGrid.JqGridHelper<jqGrid.Models.ProductViewModel>("products",
dataType: Lib.Web.Mvc.JQuery.JqGrid.JqGridDataTypes.Json,
methodType: Lib.Web.Mvc.JQuery.JqGrid.JqGridMethodTypes.Post,
pager: true,
rowsNumber: 10,
sortingName: "Id",
sortingOrder: Lib.Web.Mvc.JQuery.JqGrid.JqGridSortingOrders.Asc,
url: Url.Action("Products"),
viewRecords: true
);
}
This instance can be used for rendering HTML and JavaScript required by jqGrid:
@grid.GetHtml()
...
<script type="text/javascript">
$(document).ready(function () {
@grid.GetJavaScript()
});
</script>
All we have to do now is creating an action method, which will provide data for our grid:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Products(JqGridRequest request)
{
int totalRecordsCount = _productsRepository.GetCount();

JqGridResponse response = new JqGridResponse()
{
TotalPagesCount = (int)Math.Ceiling((float)totalRecordsCount / (float)request.RecordsCount),
PageIndex = request.PageIndex,
TotalRecordsCount = totalRecordsCount
};
response.Records.AddRange(from product in _productsRepository.FindRange(String.Format("{0} {1}", request.SortingName, request.SortingOrder), request.PageIndex * request.RecordsCount, request.RecordsCount)
select new JqGridRecord<ProductViewModel>(Convert.ToString(product.Id), new ProductViewModel(product)));

return new JqGridJsonResult() { Data= response };
}
When you run it, you should see something similar to this:
You can download a sample project for this helper, which contains following examples:
  • Basics & Formatting
  • Configuration Import/Export
  • Cell Editing
  • CRUD - Inline Editing
  • CRUD - Form Editing
  • Searching - Single
  • Searching - Toolbar
  • Searching - Custom
  • Searching - Advanced
  • Subgrid
  • TreeGrid
The helper doesn't support all the functionality of jqGrid, it's just a compilation of what I've been using. There is also no roadmap for it at the moment. If you find some necessary for you features missing or discover some bugs, please create an issue here. I will continue to develop this helper if the community will find it useful.
Older Posts