asp.net mvc 3 - Índices no secuenciales MVC3 y DefaultModelBinder
asp.net-mvc-3 modelbinders (6)
El artículo al que hizo referencia es uno antiguo (MVC2), pero hasta donde yo sé, esta sigue siendo la manera defacto de modelar colecciones de enlaces utilizando el enlazador de modelos predeterminado.
Si desea indexación no secuencial, como dice Bassam, deberá especificar un indexador. El indexador no necesita ser numérico.
Utilizamos BeginCollectionItem Html Helper de Steve Sanderson para esto. Genera automáticamente el indexador como un Guid. Creo que este es un mejor enfoque que el uso de indexadores numéricos cuando el elemento HTML de la colección no es secuencial.
¿Es cierto que la carpeta de modelo predeterminada en MVC 3.0 es capaz de manejar índices no secuenciales (tanto para tipos de modelos simples como complejos)? Me he topado con publicaciones que sugieren que sí, pero en mis pruebas parece que NO.
Dados los valores posteriores a la publicación:
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"
Y un método de controlador:
public ActionResult(IList<MyItem> items) { ... }
Los únicos valores que se cargan son los elementos 0 y 1; el elemento 4 simplemente se ignora.
He visto numerosas soluciones para generar índices personalizados ( encuadernación de modelos a una lista ), sin embargo, todos parecen apuntar a versiones anteriores de MVC, y la mayoría son un poco IMO ''pesado''.
¿Me estoy perdiendo de algo?
Este método de ayuda, derivado del enfoque de Steve Sanderson, es mucho más simple y puede usarse para anclar cualquier elemento de una colección y parece funcionar con el enlace de modelo 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;
}
Por ejemplo, simplemente llámelo en una plantilla de editor, o en cualquier otro lugar donde genere entradas, de la siguiente manera para generar la variable oculta de anclaje de índice, si corresponde.
@model SomeViewModel
@Html.AnchorIndex()
@Html.TextBoxFor(m => m.Name)
... etc.
Creo que tiene algunas ventajas sobre el enfoque de Steve Sanderson.
Funciona con EditorFor y otros mecanismos incorporados para procesar enumerables. Entonces, si
Items
es unaIEnumerable<T>
en un modelo de vista, lo siguiente funciona como se espera:<ul id="editorRows" class="list-unstyled"> @Html.EditorFor(m => m.Items) @* Each item will correctly anchor allowing for dynamic add/deletion via Javascript *@ </ul>
Es más simple y no requiere más cadenas mágicas.
Puede tener un solo EditorTemplate / DisplayTemplate para un tipo de datos y simplemente no-op si no se usa en un elemento en una lista.
El único inconveniente es que si el modelo raíz que se enlaza es el enumerable (es decir, el parámetro del método Action en sí y no simplemente una propiedad en algún lugar más profundo en el gráfico de objetos del parámetro), el enlace fallará en el primer índice no secuencial. Desafortunadamente, la funcionalidad .Index
de DefaultModelBinder solo funciona para objetos que no son raíz. En este escenario, su única opción sigue siendo utilizar los enfoques anteriores.
Estuve luchando con esto esta semana y la respuesta de Bassam fue la clave para ponerme en el camino correcto. Tengo una lista dinámica de artículos de inventario que pueden tener un campo de cantidad. Necesitaba saber cuántos de los elementos que seleccionaron, excepto la lista de elementos que puede variar de 1 a n .
Mi solución fue bastante simple al final. Creé un ViewModel llamado ItemVM con dos propiedades. ItemID y Cantidad. En la acción posterior acepto una lista de estos. Con Indexing activado, todos los elementos se pasan ... incluso con una cantidad nula. Tienes que validar y manejar el lado del servidor, pero con la iteración es trivial manejar esta lista dinámica.
En mi vista, estoy usando algo como esto:
@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" />
}
Esto me da una lista con un índice basado en 0, pero la iteración en el controlador extrae todos los datos necesarios de un nuevo modelo fuertemente tipado.
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);
}
}
Luego terminará con una colección de artículos que tienen una cantidad mayor que 0 y la identificación del artículo.
Esta técnica funciona en MVC 5 utilizando EF 6 en Visual Studio 2015. Quizás esto ayude a alguien a buscar esta solución como yo.
O use esta función de JavaScript para corregir la indexación: (Reemplace EntityName y FieldName obviamente)
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 -
}
Tengo esto funcionando, debe recordar agregar una entrada oculta de indexación común como se explica en el artículo al que se hace referencia:
La entrada oculta con name = Items.Index
es la parte clave
<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" />
espero que esto ayude
Terminé haciendo un HTML Helper más genérico: -
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());
}
}
}
Y luego incluirlo en cada iteración del elemento de lista en su vista Razor: -
@Html.HiddenIndexFor(m => m.ExistingWallboardMessages, i)