In part 1 of this series on the Telerik MVC Grid control, we discussed how to define the front-end. As a refresher, here’s a list of tasks we need to account for when implementing the grid:

  1. Adding required Telerik references, etc.
  2. Implementing the view, with the grid component, itself.
  3. Implementing several JavaScript functions to handle grid events.
  4. Implementing a View Model to support the page implementing the grid control.
  5. Implementing several controller actions to support the control.

This is not the order in which these steps must be performed; I simply just needed a way to reference each step. Essentially, we went over steps 1 through 3. In this article, we’ll focus on steps 4 and 5: basically, the back-end support steps.

Initially Displaying the Grid

I mentioned in part 1 that the grid is completely AJAX controlled, which we “forced” by not passing the model to the Grid method call. We only specify the model in the generic specification . Therefore, the DataBinding Select option will populate the grid via AJAX. Here’s that part of the view code again, for reference:

@{Html.Telerik().Grid<CustomerViewModel>()
    .Name("Customers")
    .DataKeys(keys => keys
        .Add(c => c.CustomerId)
        .RouteKey("CustomerId"))
    .DataBinding(dataBinding => dataBinding.Ajax()
            .Select("AjaxCustomersHierarchy", "Home")
            .Insert("AjaxAddCustomer", "Home")
            .Update("AjaxSaveCustomer", "Home")
            .Delete("AjaxDeleteCustomer", "Home"))

We still need to populate the “framework” of the grid, though, and that’s what the Index action method is used for in our example. Since we can ignore the route values in this simple example, we don’t have any parameters for the Index action method, nor do we need to pass in the model to the View call. Of course, a more realistic example would probably show some page title info, etc., but I’m trying to keep this relatively simple, and focus on the grid. So, there’s no need for any model data for the grid at this point:

public ActionResult Index()
{
    return View();
}

Customer View Model

In recommending we create a view model instead of working directly with the customer data entity, we’ve created a view model for Customer, called CustomerViewModel. This contains the only properties from the actual Entity Framework model our view cares about, plus any additional view-specific settings (we don’t happen to have any of those for this view).

Telerik’s MVC Grid component’s AJAX functionality respects and takes advantage of MVC’s client-side validation, so we’re specifying the [Required] attribute on a few of the properties. Since we also need a custom validator to prevent duplicate customer names, and we’re using client-side remote validation, we’re using the [Remote] attribute as well. By the way, there are complications using remote validation for the nested detail grid, which we’ll discuss later in this series. Note that any fields we mark with the [ScaffoldColumn(false)] attribute will not be displayed in the grid nor on the pop-up edit dialog used when we edit or add a customer. Keep this in mind, since this will become an issue later.

public class CustomerViewModel
{
    [ScaffoldColumn(false)]
    public int CustomerId { get; set; }

    [Required]
    [DisplayName("Account Number")]
    public string AccountNumber { get; set; }

    [Required]
    [Remote("CheckDuplicateCustomerName", 
            "Home", 
            AdditionalFields = "CustomerId, FirstName, MiddleName", 
            ErrorMessage = "This name has already been used for a customer. Please choose another name.")]
    [DisplayName("Last Name")]
    public string LastName { get; set; }

    [Required]
    [Remote("CheckDuplicateCustomerName", 
            "Home", 
            AdditionalFields = "CustomerId, LastName, MiddleName", 
            ErrorMessage = "This name has already been used for a customer. Please choose another name.")]
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [DisplayName("Middle Name")]
    [Remote("CheckDuplicateCustomerName", 
            "Home", 
            AdditionalFields = "CustomerId, LastName, FirstName", 
            ErrorMessage = "This name has already been used for a customer. Please choose another name.")]
    public string MiddleName { get; set; }

    [DisplayName("Middle Initial")]
    public string MiddleInitial { get; set; }
}

We need to set up the action methods to handle the AJAX calls produced by the grid. Since, after each call, we’re going to need to refresh the grid, we’ll first create a helper method to share across each action, that’s responsible for returning the updated data:

private IEnumerable<customerviewmodel> GetCustomers()
{
    var viewModel = new CustomersViewModel();
    var customers = from c in viewModel.Customers select c;
    return customers.ToList();
}

Here’s the CustomersViewModel that we’re using to populate the grid:

public class CustomersViewModel
{
    public IQueryable<customerviewmodel> Customers { get; set; }

    public CustomersViewModel()
    {
        var context = new SampleEntities();
        Customers = from c in context.Customers
                    orderby c.LastName, c.FirstName
                    select
                        new CustomerViewModel
                            {
                                CustomerId = c.ID,
                                FirstName = c.FirstName,
                                LastName = c.LastName,
                                MiddleName = c.MiddleName,
                                MiddleInitial = c.MiddleName == "" ? "" : " " + c.MiddleName.Substring(0, 1) + ".",
                                AccountNumber = c.AccountNumber
                            };
    }
}

Here’s the action method used by the grid’s Select DataBinding option. Notice that since the grid expects a JSON result along with a count (used for paging), you must decorate the method with [GridAction]. Otherwise, a regular view result will be returned. Think of this as an extension of JsonResult. We can’t just return a JsonResult, though, because that would fall short one returned parameter — the record count used by the grid for handling paging.

Also, we’ll need to wrap the view model (built by calling the helper method we defined above) in the Telerik library’s GridModel() call, in order to convert the model into that JSON + count wrapper object:

[GridAction]
public ActionResult AjaxCustomersHierarchy()
{
    return View(new GridModel(GetCustomers()));
}

By the way, we’re going to need to make a slight modification to the JavaScript onEditCustomers function we created in part 1. Because both the grid and the pop-up edit windows use the same [ScaffoldColumn] attribute in our view model to decide whether or not to display a property, we cannot use this attribute if we want to display this column in either the grid or the pop-up. Therefore, because we’re displaying the MiddleInitial property in the grid, but the MiddleName property instead in the pop-up window, we need a small hack to hide MiddleInitial from the pop-up window. It’s the last line of the function. Yes — yuck!

function onEditCustomers(e) {
    var popup = $("#" + e.currentTarget.id + "PopUp");
    var popupDataWin = popup.data("tWindow");

    if (e.mode == "insert")
        popupDataWin.title("Add new Customer");
    else
        popupDataWin.title("Edit Customer");

    // Hide fields from the pop-up.
    $(e.form).find("#MiddleInitial").closest(".editor-field").prev().andSelf().hide();
}

Updating Data in the Grid

Here’s the action method used for the Update DataBinding option. By the way, Telerik’s examples follow a pattern of prefixing action methods that handle AJAX calls with an underscore. They also (redundantly, in my opinion) suffix these method names with “Ajax.” I believe we only really need to use one or the other, so I’m following my own convention here. These naming conventions are totally up to you, of course.

Here, we’re using the MVC binding pattern of wrapping the UpdateModel call in a try / catch block. In this scenario, we’re using the Response object to return serious issues, which we’ll be handling in the onError grid handler on the view (more about that later). After our update logic, we’re calling the helper function to refresh the list in our returned view model:

[GridAction]
public ActionResult AjaxSaveCustomer(int customerId, string lastName, string firstName, string middleName)
{
    if (ModelState.IsValid)
    {
        try
        {
            var context = new SampleEntities();
            var customer = context.Customers.Where(c => c.ID == customerId).FirstOrDefault();
            UpdateModel(customer);
            customer.LastName = lastName;
            customer.FirstName = firstName;
            customer.MiddleName = middleName;
            context.SaveChanges();
        }
        catch (Exception e)
        {
            Response.StatusCode = 500;
            Response.AppendHeader("message",
                                    "There was an issue editing data for customer \"" + firstName + " " + lastName +
                                    "\". Please contact tech support with this message: " + e.Message);
        }
    }

    return View(new GridModel(GetCustomers()));
}

Validation

The following controller code handles the validation we specified via the [Remote] attribute in our view model. I’m sure this could be handled even more gracefully, but for now, we’ve marked all three name segments to share the same validation code. This same validation code will work for both editing an existing customer as well as adding a new one, since the ID check will never find an existing customer with an ID of zero in our table. So that portion of the logic will always be true:

public JsonResult CheckDuplicateCustomerName(CustomerViewModel model)
{
    return Json(ValidateCustomer(model), JsonRequestBehavior.AllowGet);
}

private bool ValidateCustomer(CustomerViewModel model)
{
    var context = new SampleEntities();
            
    return context.Customers
        .Where(c =>
            c.LastName.ToLower() == model.LastName.ToLower() &&
            c.FirstName.ToLower() == model.FirstName.ToLower() &&
            (model.MiddleName == null ? c.MiddleName == null : c.MiddleName.ToLower() == model.MiddleName.ToLower()) &&
            c.ID != model.CustomerId).Count() == 0;
}

Adding Data to the Grid

Here’s the action method used for the Add DataBinding option:

[GridAction]
public ActionResult AjaxAddCustomer(string lastName, string firstName, string middleName)
{
    if (ModelState.IsValid)
    {
        try
        {
            var context = new SampleEntities();
            var customer = new Customer();
            UpdateModel(customer);
            customer.LastName = lastName;
            customer.FirstName = firstName;
            customer.MiddleName = middleName;
            context.Customers.AddObject(customer);
            context.SaveChanges();
        }
        catch (Exception e)
        {
            Response.StatusCode = 500;
            Response.AppendHeader("message",
                                    "There was an issue adding customer \"" + firstName + " " + lastName +
                                    "\". Please contact tech support with this message: " + Utility.GetInnermostException(e).Message);
        }
    }

    return View(new GridModel(GetCustomers()));
}

Deleting Data from the Grid

Here’s the action method used for the Delete DataBinding option. In our example, we’re only allowing a deletion to take place if the customer doesn’t have any orders. Note that again, we’re using the response object to return an exception condition. Normally, we could return such info in the JSON object we return in the view model, but Telerik has strong control over this. Because we’re required to use the [GridAction] attribute along with wrapping our returned view model in GridModel(), we’re forced to conform to a specific object graph — the IEnumerable containing our grid data, along with the count of grid rows — leaving no place to stuff supplemental data. We could also use ViewBag (or ViewData), but when I’m using the extra data for an alert, I tend to use the response object in conjunction with onError. There’s no one correct answer:

[GridAction]
public ActionResult AjaxDeleteCustomer(int customerId)
{
    try
    {
        var context = new SampleEntities();
        var customer = context.Customers.Where(c => c.ID == customerId).First();
        var orderCount = customer.Orders.Count();

        if (orderCount > 0)
        {
            Response.StatusCode = 500;
            Response.AppendHeader("message", "This customer has order history, and cannot be deleted until all orders are deleted.");
        }
        else
        {
            context.Customers.DeleteObject(customer);
            context.SaveChanges();
        }
    }
    catch (System.Exception e)
    {
        Response.StatusCode = 500;
        Response.AppendHeader("message", "There was an issue deleting this customer. Please contact tech support with this message: " + Utility.GetInnermostException(e).Message);
    }

    return View(new GridModel(GetCustomers()));
}

Here’s the JavaScript for the onError handler I’ve referred to:

function onError(e) {
    e.preventDefault();
    alert(e.XMLHttpRequest.getResponseHeader("message"));
}

Hopefully, parts 1 and 2 will help you create the first level of a multi-hierarchical (master / detail) data grid. In part 3, we’ll go through the detail grid, and I’ll have the complete source code for this example ready for download.

Feedback

Again, please comment as you see fit. There are many “correct” ways to handle this, and I don’t claim my way to be the best. I’m just trying to share my experiences so that it can perhaps help others avoid some of the pain I went through in implementing this solution. And once again, sorry for the delay in publishing part 2 of this series. I really didn’t anticipate the requests for source code, although in hindsight I probably should have. But because my initial post was based on code I did for a client, I had to go back and create a whole new database and project for this series in order to protect their application and data. The next series I write will be completed before I post any part, so that this doesn’t happen again. I will make the source code available for download at the completion of this series. Thank you for your patience.