Introduction
If you're on this post, you have probably already heard all about Telerik’s Kendo UI and its range of front end widgets.
Here we look at the Kendo UI grid, which as its name implies, is an extensible grid for the visualisation and editing of data via a table representation. One such extension is a pop up box to add another means of editing your data.
I will show you how to modify the pop up box to perform a delete operation and compliment the existing add operation. This can be particularly useful for handling many-to-one relationships with the pop up.
Extending and exploiting Kendo’s flexibility can be fun, so let’s stuck in.
Project files
You can access a working project example here.
The project contains a grid for managing a list of food products, each of which can have any number of categories. A pop up with a dropdownlist handles either adding or removing any of the categories.
Clicking edit launches our pop up.
An error message is triggered if you try to delete a non existent category.
Add the pop up
Taking a basic grid such as the example on Kendo’s site.
Extend it’s functionality to contain a pop up by adding the following line.
.Editable(ed => ed.Mode(GridEditMode.PopUp).TemplateName("_PopUpView"))
Here we define our own pop up template called “PopUpView”.
Make a new folder in Views\Shared called Editor Templates. Inside, create a Razr partial called PopUpView.cshtml and include the following code.
@model ProductModel
<fieldset id="popUpForm">
@Html.HiddenFor(m => m.Categories)
@Html.ValidationMessageFor(m => m.CategoryId)
@Html.ValidationMessageFor(m => m.Category)
@Html.HiddenFor(m => m.Categories)
@Html.HiddenFor(m => m.Id)
<br/>
<br/>
<p>
@(Html.Kendo().DropDownListFor(m => m.CategoryId)
.AutoBind(true)
.OptionLabel("Select Category...")
//.Events(e => e.Change("onChange"))
.HtmlAttributes(new { style = "padding:10px;" })
.DataTextField("Name")
.DataValueField("Id")
.DataSource(dataSource =>
{
dataSource.Read(read => read.Action("GetCategories", "Home"))
.ServerFiltering(true);
}))
</p>
</fieldset>
I’ve enclosed the code inside of a <Fieldset>
tag. This tag typically lets you logically group form elements that share some characteristics into a visual container. See here for more information..
I also find it handy for CSS styling as well as serialising form fields for passing back to a controller, which we'll see later.
Add the “Delete” button
In order to add a “delete” button to the grid, we need to tap into the grid’s edit event with the following line:
.Events(ev => ev.Edit("addDeleteButton"))
This intercepts the event that’s fired when the grid’s edit button is clicked. This is then hooked up to the following JavaScript function:
function addDeleteButton(e) { $('<a class="k-button k-grid-add k-button-icontext custom" href="\#"k-icon k-i-delete">Delete').insertAfter(".k-grid-update"); $('.k-window-title').text("Edit Categories"); $(".custom").click(function (e) { e.preventDefault(); var formContainer = $("#popUpForm"); SubmitInfo(formContainer); }); }
This block uses jQuery’s insertAfter() function to dynamically insert a button element to the pop up at the moment it is opened.
Whilst we’re at it, we can overwrite the pop up’s default title by targeting the k-window-title and attaching the text using the .text() function.
$('.k-window-title').text("Edit Categories");
The last line attaches a click event which links to code we'll discuss in the next section.
TRIVIA : The dollar sign is commonly used as a shortcut to the function document.getElementById(). Because this function is fairly verbose and used frequently in JavaScript, the $ has long been used as its alias. Source,
The final result should visuallly look like this :
Adding AJAX
Deleting articles through the pop up is not supported natively by Kendo, however, we can get around this by collecting the data held inside our <fieldset>
tag and sending it back to its own controller method using some nifty AJAX.
Note this is only some of the code, the rest will be covered later when relevant.
function SubmitInfo(formContainer) {
$.ajax({
url: '@Url.Action("Delete", "Home")',
type: "POST",
dataType: "JSON",
data: data + "&" + formContainer.serialize(),
success: function (result) {
//Code shown later
}
});
}
Controller
Now set up the associated controller method.
public void Delete(ProductModel productModel)
{
var product = siteRepository.GetProductById(productModel.Id);
var categoryToRemove = product.Categories.SingleOrDefault(r => r.Id == productModel.CategoryId);
if (categoryToRemove != null)
{
product.Categories.Remove(categoryToRemove);
siteRepository.UpdateProduct(product);
}
else
{
this.ModelState.AddModelError("CategoryNotExist", "This Category is not assigned to this product");
this.ModelState.AddModelError("CategoryNotExist", "This is another error");
}
}
This function receives the product model, leveraging its values to return the desired product and category models.
An if
statement checks whether the selected product contains the requested category and if not, adds an error to the ModelState’s collection of errors in the controllers.
TRIVIA : Let’s recap on ModelState, which is a controller property and inherits from System.Web.Mvc.Controller. It represents the collection of name and value pairs that were submitted to the server during a POST back. It also contains a collection of error messages for each value submitted.
By default, the model binder will add some errors for basic type conversion issues (for example, if you try to bind a non-number for something which is specified "int" in a form).
It will also validate custom validation rules defined in your model and return an invalid model state if for example, a [required] property is empty on return.
Now, let’s look at the complete AJAX code.
$.ajax({
url: '@Url.Action("Delete", "Home")',
type: "POST",
dataType: "JSON",
//data: formContainer.serialize(),
data: data + "&" + formContainer.serialize(),
success: function (result) {
if (result.Errors) {
//Send error message to grid
var grid = $("#persons").data("kendoGrid");
showMessage(grid.editable.element, result.Errors.CategoryNotExist.errors[0], result.Errors.CategoryNotExist.errors);
}
else {
// Clear the input tags
formContainer.find("input[type='text']").each(function (i, element) {
$(this).val('');
});
$('#persons').data('kendoGrid').dataSource.read();
$('#persons').data('kendoGrid').refresh();
}
}
});
Error Handling
If an AJAX call is successful, we can use success:
to execute some subsequent code. Similar to finally
, in a try
/ catch
block. Whereas for example, if you were to use “error:” (we’re not in our example), that would be where your "catch" code would go.
Essentially, as Ajax is asynchronous, “success:” provides us the hook to intercept and work with the returned data.
We then call the showMessage
function passing our grid and accessing the ModelState’s error collection with JavaScript.
showMessage
finds the existingValidationMessage
for Category on the grid and replaces it with thevalidationMessageTmpl
template containing the html and styling for our custom error message.
function showMessage(container, error) {
//add the validation message to the form
container.find("[data-valmsg-for=" + "\"Category\"" + "]")
.replaceWith(validationMessageTmpl({ field: error.CategoryNotExist, message: error[0] }));
}
var validationMessageTmpl = kendo.template(
"<div id='#=message#_validationMessage' " +
"class='k-widget k-tooltip k-tooltip-validation " +
"k-invalid-msg field-validation-error' " +
"style='margin: 0.5em; display: block;' data-for='#=field#' " +
"data-val-msg-for='#=field#' role='alert'>" +
"<span class='k-icon k-warning'></span>" +
"#=message#" +
"<div class='k-callout k-callout-n'></div>" +
"</div>");
I hope you find this useful, give it a try and let know any thoughts and improvements below.