Frage

Is it true that the default model binder in MVC 3.0 is capable of handling non-sequential indices (for both simple and complex model types)? I've come across posts that suggest it should, however in my tests it appears that it does NOT.

Given post back values:

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"

And a controller method:

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

The only values that are loaded are items 0 and 1; item 4 is simply ignored.

I've seen numerous solutions to generate custom indices (Model Binding to a List), however they all appear to targeting previous versions of MVC, and most are a bit 'heavy-handed' IMO.

Am I missing something?

War es hilfreich?

Lösung

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

Andere Tipps

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)
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top