'Server-side validation for newly created rows in a Kendo grid (in-cell, batch editing mode)
I have a Kendo Grid with InCell editing that sends created/updated records to the server in batches (.Batch(true)).
Here's a pared-down example of the grid definition:
@(Html.Kendo().Grid<TagEditingGridViewModel>()
.Name("...")
.Columns(c =>
{
c.Bound(e => e.TagText);
c.Bound(e => e.Description);
})
.Editable(e => e.Mode(GridEditMode.InCell))
.DataSource(d => d
.Ajax()
.Batch(true)
.Model(m => m.Id(e => e.ID))
//.Events(e => e.Error("...").RequestEnd("..."))
// Read, Update, Create actions
)
)
The grid handles Tag items, which must have a unique, non-empty value in the TagText property.
Here's the grid's model class, with its validation attributes:
public class TagEditingGridViewModel
{
public int ID { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "A tag text is required.")]
[StringLength(50, ErrorMessage = "Text cannot be longer than 50 characters")]
public string TagText { get; set; }
[StringLength(250, ErrorMessage = "Description cannot be longer than 250 characters")]
public string Description { get; set; }
}
The [StringLength] attribute triggers client-side validation, as does the [Required] attribute when the field is empty. But server-side validation is still needed when the TagText field is whitespace only, and to check uniqueness.
This server-side validation needs to take place both on updating an existing record and on creating a new record. That's where the problem begins. For an existing record, the model has an ID in the database that can be used to find the corresponding row in the grid. But a new record that does not pass validation does not get an ID in the database and does not have a (unique) ID in the grid rows - it is set to 0, so you can't identify a row from that property.
In this post in the Kendo forums, a Telerik employee has posted a solution to showing a server-side validation error in a Kendo grid with InCell and batch editing. Unfortunately, they only show the solution on update, not on create.
In their suggested solution, they use the onError event of the grid's DataSource, where they find the the row in the grid using the model's ID field.
// Controller:
currentErrors.Add(new Error() { id = model.LookupId, errors = errorMessages });
// JavaScript:
var item = dataSource.get(error.id);
var row = grid.table.find("tr[data-uid='" + item.uid + "']");
In my create action, I loop through the incoming items and set the key in the model state dictionary to "models[i].TagText". When the TagText is a string that only contains whitespace, the [Required] attribute catches this server-side, and adds a model state error in that same format.
// items: List<TagEditingGridViewModel>
for (int i = 0; i < items.Count(); i++)
{
// check for uniqueness of TagText ...
// this is the way the validation attributes do it
ModelState.AddModelError($"models[{i}].TagText", "Tag text must be unique.");
}
return Json(items.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
In my grid, I can add a handler to the RequestEnd event, which has access to the request type (read, create, or update), the data sent back from the server (which would be items), and any model state errors.
But I still have the problem that I'm not able to map items with an ID of 0 to rows in the grid. Is there any guarantee that the items are still in the same order they were sent, and that that is the order they are in the DOM?
Solution 1:[1]
Here's how I ended up solving this issue:
I first modified my grid view model to include a property for the Kendo grid row's UID.
public string KendoRowUID { get; set; }I added two events to the grid's
DataSource(not to the grid as a whole).
In theChangeevent, when the action was"add"(when a new row is added), I set the data item'sKendoRowUIDproperty to the row's UID..DataSource(d => d // ... .Events(e => e .Change("grdEditTagsOnChange") .Error("grdEditTagsOnError") // explained in step 7 ) )function grdEditTagsOnChange(e) { // set the KendoRowUID field in the datasource object to the row uid attribute if (e.action == "add" && e.items.length) { var item = e.items[0]; item.KendoRowUID = item.uid; } }Based on what information I needed to show the
ModelStateerrors on the page, I created this method in my controller. It simply takes the fields I needed and sticks them into a JSON object string that I can later deserialize in JavaScript.
I added allModelStateerrors under the key"", so that later (step 7), they all show up undere.errors[""].private void AddGridModelError(string field, string message, string kendoRowUid, int? modelId = null) { var error = new { field, message, kendoRowUid, modelId = (modelId != null && modelId > 0) ? modelId : null }; ModelState.AddModelError("", // Newtonsoft.Json JsonConvert.SerializeObject(error, Formatting.None)); }I created this method to modify any existing
ModelStateerrors to fit the new format. This is necessary because the[Required(AllowEmptyStrings = false)]attribute does catch empty strings, but only server-side (empty strings don't get caught in client-side validation).
(This may not be the most efficient or best way to do it, but it works.)private void AlterModelError(List<TagEditingGridViewModel> items) { // stick them in this list (not straight in ModelState) // so can clear existing errors out of the modelstate easily var newErrors = new List<(string, string, string, int)>(); // get existing model state errors var modelStateErrors = ModelState.Where(ms => ms.Key != "" && ms.Value.Errors.Any()); foreach (var mse in modelStateErrors) { // the validation attributes do it like this: "models[0].TagText" if (mse.Key.Contains('.')) { var split = mse.Key.Split('.'); if (split.Length == 2) { // get index from "models[i]" part var regex = new Regex(@"models\[(\d+)\]"); var match = regex.Match(split[0]); var index = match.Groups[1].Value?.ToInt(); if (index != null) { var item = items[index.Value]; foreach (var err in mse.Value.Errors) { newErrors.Add((split[1], err.ErrorMessage, item.KendoRowUID, item.ID)); } } } } } // clear everything from the model state, and add new-format errors ModelState.Clear(); foreach (var item in newErrors) { // call the method shown in step 3: AddGridModelError(item.Item1, item.Item2, item.Item3, item.Item4); } }In the create/update grid actions, I call the
AlterModelErrormethod if there are anyModelStateerrors already present. And did additional validation as necessary.if (!ModelState.IsValid) { AlterModelError(items); } // 'item' is type: TagEditingGridViewModel AddGridModelError( nameof(TagEditingGridViewModel.TagText), "The tag text must be unique.", item.KendoRowUID, item.ID);At the end of the create/update grid actions, I made sure to include the
ModelStatedictionary when callingToDataSourceResult:return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);Finally, in the grid's
DataSource'sErrorevent, I ...Check if there are any errors in the event
errorspropertyAdd a one-time handler to the grid's
DataSourcesync eventIn that sync event handler, loop through all the errors, and
Parse the string into a JSON object
Find the
<tr>row.
If the item already exists in the database, itsIDfield can be used to get the item from theDataSource, and the row can be gotten from there. If the item was a newly created item, itsIDis still set to0, so thekendoRowUidproperty of the JSON object is used.Use the
fieldproperty of the JSON object to locate the correct column (and thus, cell) within the rowAppend an element to the cell that shows the validation message
function grdEditTagsOnError(e) { // if there are any errors if (e.errors && e.errors[""]?.errors.length) { var grid = $("#grdEditTags").data("kendoGrid"); // e.sender is the dataSource // add a one-time handler to the "sync" event e.sender.one("sync", function (e) { // loop through the errors e.errors[""].errors.forEach(err => { // try to parse error message (custom format) to a json object var errObj = JSON.parse(err); if (errObj) { if (errObj.kendoRowUid) { // find row by uid var row = grid.table.find("tr[data-uid='" + errObj.kendoRowUid + "']"); } else if (errObj.modelId) { // find row by model id var dsItem = grid.dataSource.get(errObj.modelId); var row = grid.table.find("tr[data-uid='" + dsItem.uid + "']"); } // if the row was found if (row && row.length) { // find the index of the column var column = null; for (var i = 0; i < grid.columns.length; i++) { if (grid.columns[i].field == errObj.field) { column = i; } } if (column != null) { // get the <td> cell var cell = row.find("td:eq(" + column + ")"); if (cell) { // create the validation message // in the same format as the grid's default validation elements var valMessage = '<div class="k-tooltip k-tooltip-error k-validator-tooltip k-invalid-msg field-validation-error" ' + 'data-for="' + errObj.field + '" ' + 'id="' + errObj.field + '_validationMessage" ' + 'data-valmsg-for="' + errObj.field + '">' + '<span class="k-tooltip-icon k-icon k-i-warning"></span>' + '<span class="k-tooltip-content">' + errObj.message + '</span>' + '<span class="k-callout k-callout-n"></span>' + '</div>'; // insert validation message after cell.html(cell.html() + valMessage); // make the message not cut off cell.css("overflow", "visible"); } // end 'if (cell)' } // end 'if (column != null)' } // end 'if (row && row.length)' } // end 'if (errObj)' });// end 'errors.forEach' });// end 'e.sender.one("sync", function ...' } // end if any errors } // end function
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | CarenRose |
