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:
- Adding required Telerik references, etc.
- Implementing the view, with the grid component, itself.
- Implementing several JavaScript functions to handle grid events.
- Implementing a View Model to support the page implementing the grid control.
- 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
@{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.
Great articles.
Can you by any chance share the current solution and send it as an attachment since I am about to start using Telerik MVC Grid helper.
Thanks,
Rad
Hi Rad. I’ll be posting the source code along with part 3, which I plan on publishing by next week.
Very nice work!
Looking forward to part 3.
Cheers,
Danny
Hi Mark,
Thanks for the work! You’re hitting every point that I hit, and then some. You’ve got another subscription.
-Arnold
Hey Mark,
Fantastic series. Has helped me a great deal get under the covers of the black-box like Telerik Javascript library. Really, really, really, eagerly waiting for the editable details post. When do you suppose that is coming?
Thanks again – cheers
Thanks. I’m hoping I can post part 3 by this coming weekend. Been extremely busy at work.
Mark,
great post explaining the usage of the Telerik MVC Grid.
By any chance did you experience with the Pop-up editor of the Grid, that the edit window is not centered when displayed?
Also, when are you planning on publishing Part 3 (awaiting eagerly).
Thanks!!!
~Eric.
Hello Mark,
Very interesting articles.
I want to check duplicate items entered by the user in a batch editing Telerik grid. Dou you have any sample?
Telerik batch editing sample: http://demos.telerik.com/aspnet-mvc/grid/editingbatch
Thank you.
Is there some sort of setting I’m missing to trigger the select? I can’t ever get it to load the data unless I pass it in as the model..
Eric, I manually position the pop-up editor. I’ll show some code for that in part 3, which I hope to get done in the next few days.
Oscar, I haven’t tried batch editing yet, so I’m not sure. I’m assuming the check would be done on the server, and you’d reject the entire batch (not save anything). You could also choose to save the non-duplicates, but leave the dupes in error, but you’d need to be careful on how to report that back to the user, so there isn’t confusion of what happened.
Chris, did you monitor the call to the path specified in the Select option of DataBinding with a tool like Fiddler, to make sure the AJAX call is working? Fiddler has saved me so many times, I’ve lost count.
I tried fiddler and it appears that it’s not calling it at all – like Ajax is not being called on the refresh. I see the path when I click the Add button fires off, but never when loading data. I must be missing something simple to turn it on, but I can’t tell what it is. All the examples etc. say to do what I’m doing, but it’s still not working. Guess I’ll just have to keep trying, thanks for the response.
Another common cause of the AJAX call not occurring, is a possible JavaScript error on the page that may abort the rest of the script. The “pause on exceptions” option in Chrome’s script inspector has been extremely helpful to me for that. There may be an equivalent in FireBug or IE’s Developer tools, but I don’t recall.
I got it to work in a new project created from the Telerik template, so it must be something missing from my current project (I added the Telerik controls after I started it) that is causing it to break. Thanks for the ideas, I’ll update when I figure out just what it was!
Finally found it…
Chrome was helpful in showing me that there was a javascript error occuring, I just couldn’t see it without inspecting the page. Of course it was the generic “Microsoft JScript runtime error: Unable to get value of the property ‘transport’: object is null or undefined” which I found people saying this could be a typo. So I started commenting out JavaScript, then the Client Template – and it worked again!
I had never seen/used the ClientTemplate before and when I saw ”. Once I put that in there everything worked correctly. 🙂
Mark, thanks so much for the help and the articles! I’m looking forward to Part 3!
It seems to have cut out the key part of my remark.. maybe html formatting will get past the sensor? Anyway, my problem was I missed the trailing hash. 🙂
<#=Location#>
Great to hear, Chris. I’m really hoping to finish part 3 this weekend, if I can finish some lose ends at work before then.
Hey,
I am having some issue while hiding the fields in popup widow. the textbox hides with your code but the label does not hide for me. do you have any idea?
Hi Dipak,
Make sure your jQuery is set to find both elements (the label and the textbox). The following line is how I did it:
$(e.form).find(“#CustomerId”).closest(“.editor-field”).prev().andSelf().hide()
The line first finds the label (#CustomerId), then it finds the next closest element with the .editor-field class, which is the div surrounding the textbox.
Then it finds the previous sibling, which is the div surrounding the label (itself), using .prev(). That gets pushed to the stack of elements that will be worked on by jQuery.
Next, it adds the div surrounding the label to the stack, using .andSelf().
At this point, two items are in the jQuery object stack — the div surrounding the label and the div surrounding the textbox.
Finally, the jQuery hide() function is called to hide both. Yes, it’s a bit convoluted. I got this from one of Telerik’s solutions. You can use something like Firebug or Chrome’s inspector to inspect the HTML and maybe break down the jQuery into multiple steps, if you feel it makes the code clearer.
Hi Mark,
I found that your article are very helpful for me to understand telerik for MVC, since i’m still learning about that.
Could you suggest me some article or books that can i use as reference to learn more about telerik?
Thanks for this great article 🙂
MArio
Hi Mario,
There were several books about Telerik on Amazon a few years ago, but I have not read any of them. I switched to using their KendoUI library shortly after writing these articles. I have not seen any new books about it since. Sorry.