服务器端验证 Kendo 网格中新创建的行(单元格内、批量编辑模式)

发布于 2025-01-10 18:36:53 字数 2957 浏览 0 评论 0 原文

我有一个具有 InCell 编辑功能的 Kendo 网格,可以将创建/更新的记录批量发送到服务器 (.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 属性中必须具有唯一的非空值。

下面是网格的模型类及其验证属性:

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,可用于在网格中查找相应的行。但是未通过验证的记录不会在数据库中获得ID,并且在网格中没有(唯一的)ID rows - 它设置为 0,因此您无法从该属性中识别行。

此帖子 在 Kendo 论坛中, 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 中的顺序?

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(
quot;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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

半窗疏影 2025-01-17 18:36:53

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

  1. 我首先修改了网格视图模型以包含 Kendo 网格行 UID 的属性。

    公共字符串 KendoRowUID { get;放; }
    
  2. 我向网格的DataSource 添加了两个事件(而不是向整个网格添加)。
    Change 事件中,当操作为 "add"(添加新行时)时,我将数据项的 KendoRowUID 属性设置为该行的 UID。

    .DataSource(d => d
        // ...
        .Events(e => e
            .Change("grdEditTagsOnChange")
            .Error("grdEditTagsOnError") // 在步骤 7 中进行了解释
        )
    )
    
    函数 grdEditTagsOnChange(e) {
        // 将数据源对象中的KendoRowUID字段设置为行uid属性
        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(字符串字段,字符串消息, 
                                   字符串 kendoRowUid,int?型号 ID = 空)
    {   
        var 错误=新{    
            场地,    
            信息, 
            剑道行Uid,
            modelId = (modelId != null && modelId > 0) ?型号:空 
        };
        ModelState.AddModelError("", 
            // Newtonsoft.Json
            JsonConvert.SerializeObject(错误, Formatting.None));
    }
    

  4. 我创建此方法是为了修改任何现有的 ModelState 错误以适应新格式。这是必要的,因为 [Required(AllowEmptyStrings = false)] 属性确实捕获空字符串,但仅限于服务器端(空字符串不会在客户端捕获)验证)。
    (这可能不是最有效或最好的方法,但它确实有效。)

    private void AlterModelError(List items)
    {
        // 将它们放在这个列表中(不是直接放在 ModelState 中)
        // 这样可以轻松地清除模型状态中的现有错误
        var newErrors = new List<(string, string, string, int)>();
    
        // 获取现有模型状态错误
        var modelStateErrors = ModelState.Where(ms => ms.Key != "" && ms.Value.Errors.Any());
        foreach(modelStateErrors 中的 var mse)
        {
            // 验证属性这样做:“models[0].TagText”
            if (mse.Key.Contains('.'))
            {
                var split = mse.Key.Split('.');
                if (split.Length == 2)
                {
                    // 从“models[i]”部分获取索引
                    var regex = new Regex(@"models\[(\d+)\]");
                    var match = regex.Match(split[0]);
    
                    var index = match.Groups[1].Value?.ToInt();
                    if(索引!= null)
                    {
                        var item = items[index.Value];
                        foreach(mse.Value.Errors 中的 var err)
                        {
                            newErrors.Add((split[1], err.ErrorMessage, item.KendoRowUID, item.ID));
                        }
                    }
                }
            }
        }
    
        // 清除模型状态中的所有内容,并添加新格式错误
        ModelState.Clear();
        foreach(newErrors 中的 var 项)
        {
            // 调用步骤3所示的方法:
            AddGridModelError(item.Item1, item.Item2, item.Item3, item.Item4);
        }
    }
    
  5. 在创建/更新网格操作中,如果有任何 ,我会调用 AlterModelError 方法ModelState 错误已存在。并根据需要进行额外验证。

    if (!ModelState.IsValid)
    {
        更改模型错误(项目);
    }
    
    // 'item' 的类型为:TagEditingGridViewModel
    添加网格模型错误(
        nameof(TagEditingGridViewModel.TagText), 
        "标签文本必须是唯一的。", 
        项目.KendoRowUID, 
        项目.ID);
    
  6. 在创建/更新网格操作结束时,我确保在调用 ToDataSourceResult 时包含 ModelState 字典:

    return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
    
  7. 最后,在网格的 DataSource< /code> 的 Error 事件,我...

    • 检查事件errors属性中是否有错误

    • 向网格的 DataSource 同步事件添加一次性处理程序

    • 在该同步事件处理程序中,循环遍历所有错误,并且

    • 将字符串解析为 JSON 对象

    • 找到 行。
      如果该项目已存在于数据库中,则可以使用其 ID 字段从 DataSource 获取该项目,并可以从那里获取该行。如果该项目是新创建的项目,其 ID 仍设置为 0,因此使用 JSON 对象的 kendoRowUid 属性。< /p>

    • 使用 JSON 对象的 field 属性来定位行内正确的列(以及单元格)

    • 将一个元素附加到显示验证消息的单元格

    函数 grdEditTagsOnError(e) {
      // 如果有任何错误
      if (e.errors && e.errors[""]?.errors.length) {
        var grid = $("#grdEditTags").data("kendoGrid");
    
        // e.sender 是数据源
        // 将一次性处理程序添加到“sync”事件
        e.sender.one("同步", 函数 (e) {
    
          // 循环遍历错误
          e.errors[""].errors.forEach(err => {
            // 尝试将错误消息(自定义格式)解析为 json 对象
            var errObj = JSON.parse(err);
    
            如果(错误对象){
              如果(errObj.kendoRowUid){
                // 根据uid查找行
                var row = grid.table.find("tr[data-uid='" + errObj.kendoRowUid + "']");
              } else if (errObj.modelId) {
                // 按模型 ID 查找行
                var dsItem = grid.dataSource.get(errObj.modelId);
                var row = grid.table.find("tr[data-uid='" + dsItem.uid + "']");
              }
    
              // 如果找到该行
              if (row && row.length) {
                // 找到列的索引
                var 列 = null;
                for (var i = 0; i < grid.columns.length; i++) {
                  if (grid.columns[i].field == errObj.field) {
                    列=我;
                  }
                }
    
                如果(列!=空){
                  // 获取细胞
                  var cell = row.find("td:eq(" + column + ")");
                  如果(单元格){
                    // 创建验证消息
                    // 与网格默认验证元素的格式相同
                    var valMessage =
                      '
    ' + '' + '' + errObj.message + '' + '' + '
    '; // 之后插入验证消息 cell.html(cell.html() + valMessage); // 使消息不被切断 cell.css("溢出", "可见"); } // 结束 'if(单元格)' } // 结束 'if (column != null)' } // 结束 'if (row && row.length)' } // 结束 'if (errObj)' });// 结束 'errors.forEach' });// 结束 'e.sender.one("sync", function ...' } // 如果有错误则结束 } // 结束函数

Here's how I ended up solving this issue:

  1. I first modified my grid view model to include a property for the Kendo grid row's UID.

    public string KendoRowUID { get; set; }
    
  2. I added two events to the grid's DataSource (not to the grid as a whole).
    In the Change event, when the action was "add" (when a new row is added), I set the data item's KendoRowUID property 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;
        }
    }
    
  3. Based on what information I needed to show the ModelState errors 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 all ModelState errors under the key "", so that later (step 7), they all show up under 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. I created this method to modify any existing ModelState errors 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);
        }
    }
    
  5. In the create/update grid actions, I call the AlterModelError method if there are any ModelState errors 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);
    
  6. At the end of the create/update grid actions, I made sure to include the ModelState dictionary when calling ToDataSourceResult:

    return Json(result.ToDataSourceResult(request, ModelState), JsonRequestBehavior.AllowGet);
    
  7. Finally, in the grid's DataSource's Error event, I ...

    • Check if there are any errors in the event errors property

    • Add a one-time handler to the grid's DataSource sync event

    • In 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, its ID field can be used to get the item from the DataSource, and the row can be gotten from there. If the item was a newly created item, its ID is still set to 0, so the kendoRowUid property of the JSON object is used.

    • Use the field property of the JSON object to locate the correct column (and thus, cell) within the row

    • Append 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
    
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文