MVC3 Non-Sequential Indices and DefaultModelBinder

烈酒焚心 提交于 2019-11-26 15:56:12

I have this working, you have to remember to add a common indexing hidden input as explained in your referenced article:

The hidden input with name = Items.Index is the key part

<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" />

hope this helps

This helper method, derived from Steve Sanderson's approach, is much simpler and can be used to anchor any item in a collection and it seems to work with MVC model binding.

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;
}

E.g. Simply call it in an EditorTemplate, or anywhere else you would generate inputs, as follows to generate the index anchoring hidden variable if one is applicable.

@model SomeViewModel
@Html.AnchorIndex()
@Html.TextBoxFor(m => m.Name)
... etc.

I think it has a few advantages over Steve Sanderson's approach.

  1. It works with EditorFor and other inbuilt mechanisms for processing enumerables. So if Items is an IEnumerable<T> property on a view model, the following works as expected:

    <ul id="editorRows" class="list-unstyled"> @Html.EditorFor(m => m.Items) @* Each item will correctly anchor allowing for dynamic add/deletion via Javascript *@ </ul>

  2. It is simpler and doesn't require any more magic strings.

  3. You can have a single EditorTemplate/DisplayTemplate for a data type and it will simply no-op if not used on an item in a list.

The only downside is that if the root model being bound is the enumerable (i.e. the parameter to the Action method itself and not simply a property somewhere deeper in the parameter object graph), the binding will fail at the first non-sequential index. Unfortunately, the .Index functionality of the DefaultModelBinder only works for non-root objects. In this scenario, your only option remains to use the approaches above.

The article you referenced is an old one (MVC2), but as far as I know, this is still the defacto way to model bind collections using the default modelbinder.

If you want non-sequential indexing, like Bassam says, you will need to specify an indexer. The indexer does not need to be numeric.

We use Steve Sanderson's BeginCollectionItem Html Helper for this. It automatically generates the indexer as a Guid. I think this is a better approach than using numeric indexers when your collection item HTML is non-sequential.

I was struggling with this this week and Bassam's answer was the key to getting me on the right track. I have a dynamic list of inventory items that can have a quantity field. I needed to know how many of which items they selected, except the list of items can vary from 1 to n.

My solution was rather simple in the end. I created a ViewModel called ItemVM with two properties. ItemID and Quantity. In the post action I accept a list of these. With Indexing on, all items get passed.. even with a null quantity. You have to validate and handle it server side, but with iteration it's trivial to handle this dynamic list.

In my View I am using something like this:

@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" />
}

This gives me a List with a 0-based Index, but iteration in the controller extracts all the necessary data from a new strongly-typed model.

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);
                }
            }

You will then end up with a collection of Items that have a quantity greater than 0, and the Item ID.

This technique is working in MVC 5 utilizing EF 6 in Visual Studio 2015. Maybe this will help someone searching for this solution like I was.

Or use this javascript function to fix the indexing: (Replace EntityName and FieldName obviously)

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 -
    }

I ended up making a more generic HTML Helper:-

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());
        }
    }
}

And then include it in each iteration of the list element in your Razor view:-

@Html.HiddenIndexFor(m => m.ExistingWallboardMessages, i)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!