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 中的顺序?

以下是我最终解决此问题的方法:

  1. 我首先修改了我的网格视图模型,为 Kendo 网格行的 UID 添加了一个 属性。

    public string KendoRowUID { get; set; }
    
  2. 我向网格的 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;
        }
    }
    
  3. 根据在页面上显示 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));
    }
    
  4. 我创建此方法是为了修改任何现有的 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);
        }
    }
    
  5. 在 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);
    
  6. 在 create/update 网格操作的末尾,我确保在调用 ToDataSourceResult:

    时包含 ModelState 字典
    return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
    
  7. 最后,在网格DataSourceError事件中,我...

    • 检查事件是否有错误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