¿Cómo producir índices de colección de prefijos no secuenciales con plantillas MVC HTML Editor?

El siguiente código se ha simplificado mucho, pero básicamente lo que busco lograr es lo siguiente:

Me gustaría poder editar las preguntas y las opciones de respuesta que contienen, mientras puedo agregar/eliminar dinámicamente preguntas/opciones de respuesta de la página. Idealmente, HtmlFieldPrefix para mis elementos no sería secuencial, pero Html.EditorFor() usa un índice secuencial.

Tengo un modelo de vista de preguntas que contiene un IEnumerable de opciones de respuesta:

public class QuestionViewModel
{
    public int QuestionId { get; set; }
    public IEnumerable<AnswerChoiceViewModel> AnswerChoices { get; set; }
}

En mi vista parcial de Pregunta (Question.ascx), tengo esto:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.QuestionViewModel>" %>

<%=Html.HiddenFor(m => m.QuestionId)%>
<%=Html.EditorFor(m => m.AnswerChoices) %>

Y la plantilla del editor de opciones de respuesta (AnswerChoiceViewModel.ascx):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.AnswerChoiceViewModel>" %>

<%=Html.HiddenFor(m => m.AnswerChoiceId)%>
<%=Html.TextBoxFor(m => m.Name)%>

Cuando renderizo Question.ascx, la salida se verá de la siguiente manera:

<input type="hidden" id="QuestionId" value="1" />
<input type="hidden" id="Question.AnswerChoices[0].AnswerChoiceId" value="1" />
<input type="hidden" id="Question.AnswerChoices[0].Name" value="Answer Choice 1" />

<input type="hidden" id="QuestionId" value="2" />
<input type="hidden" id="Question.AnswerChoices[1].AnswerChoiceId" value="2" />
<input type="hidden" id="Question.AnswerChoices[1].Name" value="Answer Choice 2" />

Lo que quiero saber es cómo puedo proporcionar a EditorFor un índice GUID personalizado para que la página se muestre así:

<input type="hidden" id="QuestionId" value="1" />
<input type="hidden" id="Question.AnswerChoices[e1424d5e-5585-413c-a1b0-595f39747876].AnswerChoiceId" value="1" />
<input type="hidden" id="Question.AnswerChoices[e1424d5e-5585-413c-a1b0-595f39747876].Name" value="Answer Choice 1" />

<input type="hidden" id="QuestionId" value="2" />
<input type="hidden" id="Question.AnswerChoices[633db1c3-f1e6-470b-9c7f-c138f2d9fa71].AnswerChoiceId" value="2" />
<input type="hidden" id="Question.AnswerChoices[633db1c3-f1e6-470b-9c7f-c138f2d9fa71].Name" value="Answer Choice 2" />

Ya he escrito un método auxiliar que obtendrá el índice de prefijo del contexto actual y lo almacenará en un campo oculto ".Índice" para que los índices no secuenciales se puedan vincular correctamente. Solo quiero saber cómo EditorFor asigna los índices para que pueda anularlo (o cualquier otra solución de trabajo).

preguntado el 29 de junio de 12 a las 19:06

5 Respuestas

Hace un tiempo abordé este problema y me encontré con una publicación de S. Sanderson (creador de Knockoutjs) donde describía y resolvía un problema similar. Usé porciones de su código y traté de modificarlo para adaptarlo a mis necesidades. Pongo el código a continuación en alguna clase (ejemplo: Helpers.cs), agrego el espacio de nombres en web.config.

    #region CollectionItem helper
    private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

    public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
    {
        var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
        string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

        // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
        html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, itemIndex));

        return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
    }

    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
    {
        return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
    {
        // We need to use the same sequence of IDs following a server-side validation failure,  
        // otherwise the framework won't render the validation error messages next to each item.
        string key = idsToReuseKey + collectionName;
        var queue = (Queue<string>)httpContext.Items[key];
        if (queue == null)
        {
            httpContext.Items[key] = queue = new Queue<string>();
            var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
            if (!string.IsNullOrEmpty(previouslyUsedIds))
                foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                    queue.Enqueue(previouslyUsedId);
        }
        return queue;
    }

    private class HtmlFieldPrefixScope : IDisposable
    {
        private readonly TemplateInfo templateInfo;
        private readonly string previousHtmlFieldPrefix;

        public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
        {
            this.templateInfo = templateInfo;

            previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
            templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
        }

        public void Dispose()
        {
            templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
        }
    }

    #endregion

Después de que pueda tener EditorTemplate o parcial como este

@using (Html.BeginCollectionItem("AnswerChoices"))
{
@Html.HiddenFor(m => m.AnswerChoiceId)
@Html.TextBoxFor(m => m.Name)
}

Y enumere a través de su plantilla de representación de lista (parcial).

Respondido el 29 de junio de 12 a las 20:06

Me tomó mucho más tiempo del que debería darme cuenta de esto. Todo el mundo está trabajando demasiado duro para hacer esto. La salsa secreta son estas cuatro líneas de código:

        @{
            var index = Guid.NewGuid();
            var prefix = Regex.Match(ViewData.TemplateInfo.HtmlFieldPrefix, @"^(.+)\[\d+\]$").Groups[1].Captures[0].Value;
            //TODO add a ton of error checking and pull this out into a reusable class!!!!
            ViewData.TemplateInfo.HtmlFieldPrefix = prefix + "[" + index + "]";
        }
        <input type="hidden" name="@(prefix).Index" value="@index"/>

Ahora, ¿qué está haciendo esto? Obtenemos un nuevo guid, este es nuestro nuevo índice para reemplazar el número entero que se asigna automáticamente. A continuación, obtenemos el prefijo de campo predeterminado y eliminamos el índice int que no queremos. Después de reconocer que hemos creado una deuda técnica, actualizamos los datos de vista para que todos los editores de llamadas ahora lo usen como el nuevo prefijo. Por último, añadimos una entrada que se envía de nuevo al enlazador de modelos especificando el índice que debe usar para enlazar estos campos.

¿Dónde tiene que ocurrir esta magia? Dentro de su plantilla de editor: /Views/Shared/EditorTemplates/Phone.cshtml

@using TestMVC.Models
@using System.Text.RegularExpressions
@model Phone
    <div class="form-horizontal">
        <hr />
        @{
            var index = Guid.NewGuid();
            var prefix = Regex.Match(ViewData.TemplateInfo.HtmlFieldPrefix, @"^(.+)\[\d+\]$").Groups[1].Captures[0].Value;
            //TODO add a ton of error checking and pull this out into a reusable class!!!!
            ViewData.TemplateInfo.HtmlFieldPrefix = prefix + "[" + index + "]";
        }
        <input type="hidden" name="@(prefix).Index" value="@index"/>
        <div class="form-group">
            @Html.LabelFor(model => model.Number, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Number, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Number, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.IsEnabled, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <div class="checkbox">
                    @Html.EditorFor(model => model.IsEnabled)
                    @Html.ValidationMessageFor(model => model.IsEnabled, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Details, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextAreaFor(model => model.Details, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Details, "", new { @class = "text-danger" })
            </div>
        </div>
    </div>

¿Plantilla del editor? ¡¿Qué?! ¡¿Cómo?! Simplemente colóquelo en el directorio mencionado anteriormente usando el nombre del objeto para el nombre del archivo. Deje que la convención MVC haga su magia. Desde su vista principal, simplemente agregue el editor para esa propiedad IEnumerable:

<div class="form-group">
@Html.LabelFor(model => model.Phones, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
    @Html.EditorFor(model => model.Phones, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>

Ahora, de vuelta en su controlador, asegúrese de actualizar la firma de su método para aceptar ese ienumerable (Bind include Phones):

        [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "ContactId,FirstName,LastName,Phones")] Contact contact)
    {
        if (ModelState.IsValid)
        {

            db.Contacts.Add(contact);
            db.SaveChanges();
            //TODO need to update this to save phone numbers
            return RedirectToAction("Index");
        }

        return View(contact);
    }

¿Cómo se agregan y eliminan en la página? Agregue algunos botones, enlace JavaScript, agregue un método al controlador que devolverá una vista para ese modelo. Ajax vuelve a agarrarlo e insertarlo en la página. Te dejaré trabajar en esos detalles, ya que en este momento es solo un trabajo ocupado.

Respondido 14 Oct 16, 19:10

Html.EditorFor no es nada más que un llamado método de ayuda Html, que representa input con todos los atributos apropiados.

La única solución que se me ocurre es escribir la propia. Debe ser bastante simple - 5-10 líneas ling. Mira esto Creación de ayudantes Html personalizados Mvc.

Respondido el 29 de junio de 12 a las 20:06

Steve Sanderson ha proporcionado una implementación simple que puede hacer lo que estás buscando. Recientemente comencé a usarlo yo mismo; no es perfecto, pero funciona. Tienes que hacer un poco de encordado mágico para usar su BeginCollectionItem método, por desgracia; Estoy tratando de solucionar eso yo mismo.

Respondido el 29 de junio de 12 a las 20:06

Otra opción es anular el atributo id de esta manera:

@Html.TextBoxFor(m => m.Name, new { id = @guid })

Respondido el 29 de junio de 12 a las 20:06

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.