MVC3非连续索引和DefaultModelBinder

47

在MVC 3.0中, 默认的模型绑定器是否能够处理非连续索引(对于简单和复杂模型类型都适用)?我看到过一些帖子表明它可以,但在我的测试中似乎并不是这样。

给定回发值:

items[0].Id = 10
items[0].Name = "Some Item"
items[1].Id = 3
items[1].Name = "Some Item"
items[4].Id = 6
items[4].Name = "Some Item"

还有一个控制器方法:

public ActionResult(IList<MyItem> items) { ... }

只有0和1号项被加载;第4项被简单忽略了。

我看过许多生成自定义索引的解决方案(绑定到列表模型),但它们似乎都针对早期版本的MVC,并且大多数在我看来都有些“笨重”。

我是否遗漏了什么?

6个回答

80

我已经做到了,你需要记得添加一个常用索引的隐藏输入框,就像你参考文章中所解释的那样:

具有 name = Items.Index 的隐藏输入框是关键部分。

<input type="hidden" name="Items.Index" value="0" />
<input type="text" name="Items[0].Name" value="someValue1" />

<input type="hidden" name="Items.Index" value="1" />
<input type="text" name="Items[1].Name" value="someValue2" />

<input type="hidden" name="Items.Index" value="3" />
<input type="text" name="Items[3].Name" value="someValue3" />

<input type="hidden" name="Items.Index" value="4" />
<input type="text" name="Items[4].Name" value="someValue4" />

希望这可以帮到你


5
哦我的天啊!这简直让返回列表变得非常容易,不用再进行繁琐的 for (i++) 循环了,我只需要使用主键或索引上的任何其他 ID,列表就会非常漂亮和强类型化地返回。这些黑暗、隐藏的秘密。这真是让我开心一整天!加1个赞!+啤酒!!! - Piotr Kula
@Yablargo,那可能是ModelState功能。你在post操作中使用它了吗? - ps2goat
@ps2goat,我对这个评论不是很确定,我打算将其删除。自从那时起,我一直在使用这个功能,无法真正了解当时遇到的问题。 - Yablargo
是否有一个像Knockout这样的客户端框架,可以为DefaultModelBinder生成和管理这些索引器? - avo
在 PartialView 中实现此功能(即每个部分是列表中的一个项目),我使用了以下代码:<input type="hidden" name="@(ViewData.TemplateInfo.HtmlFieldPrefix.Substring(0, ViewData.TemplateInfo.HtmlFieldPrefix.LastIndexOf("["))).Index" value="@ViewData.TemplateInfo.HtmlFieldPrefix.Split("[]".ToCharArray, StringSplitOptions.RemoveEmptyEntries).Last" /> - apc
显示剩余2条评论

6
您所提到的文章是一篇旧文(MVC2),但据我所知,这仍然是使用默认模型绑定器绑定集合的事实标准方法。
如果您想要非顺序索引,就像Bassam所说,您需要指定一个索引器。索引器不需要是数字。
我们使用Steve Sanderson's BeginCollectionItem Html Helper来实现此目的。它会自动生成索引器作为Guid。我认为,当您的集合项HTML是非顺序的时,这比使用数字索引器更好。

我看到了同样的文章,它确实解决了我所描述的问题。如上所述,我希望默认的模型绑定器可以在内部处理这种情况,而不需要实际使用BeginCollectionItem助手。感谢您的回复! - mindlessgoods

5

这个辅助方法是从Steve Sanderson的方法中衍生出来的,非常简单并且可以用于锚定集合中的任何项,似乎与MVC模型绑定一起使用。

public static IHtmlString AnchorIndex(this HtmlHelper html)
{
    var htmlFieldPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
    var m = Regex.Match(htmlFieldPrefix, @"([\w]+)\[([\w]*)\]");
    if (m.Success && m.Groups.Count == 3)
        return
            MvcHtmlString.Create(
                string.Format(
                    "<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
                    m.Groups[1].Value, m.Groups[2].Value));
    return null;
}

例如,只需在EditorTemplate或任何生成输入的位置调用它,如下所示,以生成适用的索引锚定隐藏变量。
@model SomeViewModel
@Html.AnchorIndex()
@Html.TextBoxFor(m => m.Name)
... etc.

我认为这种方法比Steve Sanderson的方法有一些优点:

  1. 它可以与EditorFor和其他内置机制一起处理可枚举类型。因此,如果Items是视图模型中的一个IEnumerable<T>属性,则以下内容按预期工作:

    <ul id="editorRows" class="list-unstyled"> @Html.EditorFor(m => m.Items) @*每个项目都将正确锚定,通过Javascript允许动态添加/删除*@ </ul>

  2. 这种方法更简单,不需要任何魔法字符串。

  3. 您可以为数据类型拥有一个单独的EditorTemplate/DisplayTemplate,如果没有在列表项上使用,则它将无操作。

唯一的缺点是,如果要绑定的根模型是可枚举的(即Action方法本身的参数而不仅仅是参数对象图中的某个更深层次的属性),则绑定将在第一个非连续索引处失败。不幸的是,默认的ModelBinder的.Index功能仅适用于非根对象。在这种情况下,您的唯一选择仍然是使用上述方法。


非常感谢您,Phil。我非常致力于使用EditorFor与可枚举对象,并且这个方法非常有效! - Jono

2

我这个星期一直在苦恼,Bassam的答案是让我找到正确方向的关键。我有一个动态的库存物品列表,可以拥有数量字段。我需要知道他们选择了多少个哪些物品,但是物品列表可能从1到n

最后我的解决方案相当简单。我创建了一个名为ItemVM的ViewModel,其中有两个属性:ItemID和Quantity。在发布操作中,我接受这些条目的列表。使用索引,所有项都会被传递...即使它的数量为空。你必须在服务器端验证和处理它,但通过迭代处理这个动态列表变得很容易。

在我的视图中,我正在使用类似这样的内容:

@foreach (Item item in Items)
{
<input type="hidden" name="OrderItems.Index" value="@item.ItemID" />
<input type="hidden" name="OrderItems[@item.ItemID].ItemID" value="@item.ItemID" />
<input type="number" name="OrderItems[@item.ItemID].Quantity" />
}

这给我提供了一个基于0的索引列表,但是在控制器中迭代会从一个新的强类型模型中提取所有必要的数据。

public ActionResult Marketing(List<ItemVM> OrderItems)
...
        foreach (ItemVM itemVM in OrderItems)
            {
                OrderItem item = new OrderItem();
                item.ItemID = Convert.ToInt16(itemVM.ItemID);
                item.Quantity = Convert.ToInt16(itemVM.Quantity);
                if (item.Quantity > 0)
                {
                    order.Items.Add(item);
                }
            }

你将得到一个具有数量大于0和物品ID的项目集合。
这种技术在使用Visual Studio 2015中的EF 6的MVC 5中有效。也许这会帮助像我一样正在寻找解决方案的人。

你能提供完整的例子吗?我有非常类似的情况。 - Mr_LinDowsMac

1

或者使用以下 JavaScript 函数来修复索引:(显然替换 EntityName 和 FieldName)

function fixIndexing() {
        var tableRows = $('#tblMyEntities tbody tr');

        for (x = 0; x < tableRows.length; x++) {
            tableRows.eq(x).attr('data-index', x);

            tableRows.eq(x).children('td:nth-child(1)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName1");

            tableRows.eq(x).children('td:nth-child(2)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName2");

            tableRows.eq(x).children('td:nth-child(3)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName3");
        }

        return true; //- Submit Form -
    }

1
我最终制作了一个更通用的HTML助手:-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;

namespace Wallboards.Web.Helpers
{
    /// <summary>
    /// Hidden Index Html Helper
    /// </summary>
    public static class HiddenIndexHtmlHelper
    {
        /// <summary>
        /// Hiddens the index for.
        /// </summary>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="expression">The expression.</param>
        /// <param name="index">The Index</param>
        /// <returns>Returns Hidden Index For</returns>
        public static MvcHtmlString HiddenIndexFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int index)
        {
            var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var propName = metadata.PropertyName;

            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("<input type=\"hidden\" name=\"{0}.Index\" autocomplete=\"off\" value=\"{1}\" />", propName, index);

            return MvcHtmlString.Create(sb.ToString());
        }
    }
}

然后在Razor视图中的每个列表元素的迭代中包含它:

@Html.HiddenIndexFor(m => m.ExistingWallboardMessages, i)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接