Kendo 网格中新创建的行的服务器端验证(单元格内,批量编辑模式)
Server-side validation for newly created rows in a Kendo grid (in-cell, batch editing mode)
我有一个带有 InCell
编辑功能的 Kendo 网格,可以将 created/updated 条记录分批发送到服务器 (.Batch(true)
)。
这是网格定义的简化示例:
@(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
)
)
网格处理 Tag
项,这些项在 TagText
属性.
中必须具有唯一的非空值
这是网格模型 class,及其验证属性:
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; }
}
[StringLength]
属性触发客户端验证,当字段为空时 [Required]
属性也是如此。但是当 TagText
字段只有空格时,仍然需要服务器端验证,并检查唯一性。
此服务器端验证需要在更新现有记录和创建新记录时进行。这就是问题的开始。对于现有记录,模型在数据库中有一个 ID
,可用于在网格中查找相应的行。但是未通过验证的 new 记录不会在数据库中获得 ID
并且在网格行中没有(唯一的)ID
- 它设置为 0
,因此您无法从 属性.
中识别行
在 Kendo 论坛的 this post 中,一名 Telerik 员工发布了一个解决方案,用于在 InCell
的 Kendo 网格中显示服务器端验证错误,并且批量编辑。不幸的是,他们只在更新时显示解决方案,而不是在创建时显示。
在他们建议的解决方案中,他们使用网格数据源的 onError
事件,他们在其中使用模型的 ID 字段找到网格中的行。
// 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 + "']");
在我的创建操作中,我遍历传入的项目并将模型状态字典中的键设置为“models[i].TagText
”。当 TagText
是仅包含空格的字符串时,[Required]
属性会捕获此服务器端,并以相同的格式添加模型状态错误。
// 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);
在我的网格中,我可以向 RequestEnd
事件添加一个处理程序,它可以访问请求类型(读取、创建或更新)、从服务器发回的数据(这将是items
),以及任何模型状态错误。
但我仍然遇到无法将 ID 为 0 的项目映射到网格中的行的问题。是否可以保证 items
仍然按照发送时的相同顺序,这就是它们在 DOM 中的顺序?
以下是我最终解决此问题的方法:
我首先修改了我的网格视图模型,为 Kendo 网格行的 UID 添加了一个 属性。
public string KendoRowUID { get; set; }
我向网格的 DataSource
添加了两个事件(而不是整个网格)。
在 Change
事件中,当操作为 "add"
时(添加新行时),我将数据项的 KendoRowUID
属性 设置为该行的 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;
}
}
根据在页面上显示 ModelState
错误所需的信息,我在我的控制器中创建了这个方法。它只是获取我需要的字段并将它们粘贴到一个 JSON 对象字符串中,我稍后可以在 JavaScript.
中反序列化该对象字符串
我在键 ""
下添加了所有 ModelState
错误,以便稍后(第 7 步)它们都显示在 e.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));
}
我创建此方法是为了修改任何现有的 ModelState
错误以适应新格式。这是必要的,因为 [Required(AllowEmptyStrings = false)]
属性 会 捕获空字符串,但只能捕获 server-side (空字符串不会在 client-side 验证中捕获)。
(这可能不是最有效或最好的方法,但它确实有效。)
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);
}
}
在 create/update 网格操作中,如果已经存在任何 ModelState
错误,我将调用 AlterModelError
方法。并根据需要进行额外验证。
if (!ModelState.IsValid)
{
AlterModelError(items);
}
// 'item' is type: TagEditingGridViewModel
AddGridModelError(
nameof(TagEditingGridViewModel.TagText),
"The tag text must be unique.",
item.KendoRowUID,
item.ID);
在 create/update 网格操作的末尾,我确保在调用 ToDataSourceResult
:
时包含 ModelState
字典
return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
最后,在网格DataSource
的Error
事件中,我...
检查事件是否有错误errors
属性
向网格的 DataSource
同步事件添加 one-time 处理程序
在该同步事件处理程序中,遍历所有错误,并且
将字符串解析为JSON对象
找到 <tr>
行。
如果项目已经存在于数据库中,它的 ID
字段可用于从 DataSource
中获取项目,并且可以从那里获取行。如果该项目是新创建的项目,其 ID
仍设置为 0
,因此使用 JSON 对象的 kendoRowUid
属性。
使用 JSON 对象的 field
属性 在行中定位正确的列(以及单元格)
将元素附加到显示验证消息的单元格
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
我有一个带有 InCell
编辑功能的 Kendo 网格,可以将 created/updated 条记录分批发送到服务器 (.Batch(true)
)。
这是网格定义的简化示例:
@(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
)
)
网格处理 Tag
项,这些项在 TagText
属性.
这是网格模型 class,及其验证属性:
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; }
}
[StringLength]
属性触发客户端验证,当字段为空时 [Required]
属性也是如此。但是当 TagText
字段只有空格时,仍然需要服务器端验证,并检查唯一性。
此服务器端验证需要在更新现有记录和创建新记录时进行。这就是问题的开始。对于现有记录,模型在数据库中有一个 ID
,可用于在网格中查找相应的行。但是未通过验证的 new 记录不会在数据库中获得 ID
并且在网格行中没有(唯一的)ID
- 它设置为 0
,因此您无法从 属性.
在 Kendo 论坛的 this post 中,一名 Telerik 员工发布了一个解决方案,用于在 InCell
的 Kendo 网格中显示服务器端验证错误,并且批量编辑。不幸的是,他们只在更新时显示解决方案,而不是在创建时显示。
在他们建议的解决方案中,他们使用网格数据源的 onError
事件,他们在其中使用模型的 ID 字段找到网格中的行。
// 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 + "']");
在我的创建操作中,我遍历传入的项目并将模型状态字典中的键设置为“models[i].TagText
”。当 TagText
是仅包含空格的字符串时,[Required]
属性会捕获此服务器端,并以相同的格式添加模型状态错误。
// 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);
在我的网格中,我可以向 RequestEnd
事件添加一个处理程序,它可以访问请求类型(读取、创建或更新)、从服务器发回的数据(这将是items
),以及任何模型状态错误。
但我仍然遇到无法将 ID 为 0 的项目映射到网格中的行的问题。是否可以保证 items
仍然按照发送时的相同顺序,这就是它们在 DOM 中的顺序?
以下是我最终解决此问题的方法:
我首先修改了我的网格视图模型,为 Kendo 网格行的 UID 添加了一个 属性。
public string KendoRowUID { get; set; }
我向网格的
DataSource
添加了两个事件(而不是整个网格)。
在Change
事件中,当操作为"add"
时(添加新行时),我将数据项的KendoRowUID
属性 设置为该行的 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; } }
根据在页面上显示
下ModelState
错误所需的信息,我在我的控制器中创建了这个方法。它只是获取我需要的字段并将它们粘贴到一个 JSON 对象字符串中,我稍后可以在 JavaScript.
中反序列化该对象字符串 我在键""
下添加了所有ModelState
错误,以便稍后(第 7 步)它们都显示在e.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)); }
我创建此方法是为了修改任何现有的
ModelState
错误以适应新格式。这是必要的,因为[Required(AllowEmptyStrings = false)]
属性 会 捕获空字符串,但只能捕获 server-side (空字符串不会在 client-side 验证中捕获)。
(这可能不是最有效或最好的方法,但它确实有效。)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); } }
在 create/update 网格操作中,如果已经存在任何
ModelState
错误,我将调用AlterModelError
方法。并根据需要进行额外验证。if (!ModelState.IsValid) { AlterModelError(items); } // 'item' is type: TagEditingGridViewModel AddGridModelError( nameof(TagEditingGridViewModel.TagText), "The tag text must be unique.", item.KendoRowUID, item.ID);
在 create/update 网格操作的末尾,我确保在调用
时包含ToDataSourceResult
:ModelState
字典return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
最后,在网格
DataSource
的Error
事件中,我...检查事件是否有错误
errors
属性向网格的
DataSource
同步事件添加 one-time 处理程序在该同步事件处理程序中,遍历所有错误,并且
将字符串解析为JSON对象
找到
<tr>
行。
如果项目已经存在于数据库中,它的ID
字段可用于从DataSource
中获取项目,并且可以从那里获取行。如果该项目是新创建的项目,其ID
仍设置为0
,因此使用 JSON 对象的kendoRowUid
属性。使用 JSON 对象的
field
属性 在行中定位正确的列(以及单元格)将元素附加到显示验证消息的单元格
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