Vistas parciales de ASP.NET MVC: prefijos de nombre de entrada

120

Supongamos que tengo ViewModel como

public class AnotherViewModel
{
   public string Name { get; set; }
}
public class MyViewModel
{
   public string Name { get; set; }
   public AnotherViewModel Child { get; set; }
   public AnotherViewModel Child2 { get; set; }
}

En la vista puedo renderizar un parcial con

<% Html.RenderPartial("AnotherViewModelControl", Model.Child) %>

En el parcial lo haré

<%= Html.TextBox("Name", Model.Name) %>
or
<%= Html.TextBoxFor(x => x.Name) %>

Sin embargo, el problema es que ambos representarán name = "Name" mientras que yo necesito tener name = "Child.Name" para que la carpeta de modelos funcione correctamente. O, name = "Child2.Name" cuando renderizo la segunda propiedad usando la misma vista parcial.

¿Cómo hago para que mi vista parcial reconozca automáticamente el prefijo requerido? Puedo pasarlo como parámetro, pero esto es demasiado inconveniente. Esto es aún peor cuando quiero, por ejemplo, renderizarlo de forma recursiva. ¿Hay alguna manera de representar vistas parciales con un prefijo o, mejor aún, con la reconición automática de la expresión lambda de llamada para que

<% Html.RenderPartial("AnotherViewModelControl", Model.Child) %>

agregará automáticamente el "Niño" correcto. prefijo a las cadenas de nombre / identificación generadas?

Puedo aceptar cualquier solución, incluidos los motores de vista de terceros y las bibliotecas. De hecho, uso Spark View Engine ("resuelvo" el problema usando sus macros) y MvcContrib, pero no encontré una solución allí. XForms, InputBuilder, MVC v2: cualquier herramienta / conocimiento que proporcione esta funcionalidad será excelente.

Actualmente pienso en codificar esto yo mismo, pero parece una pérdida de tiempo, no puedo creer que estas cosas triviales no estén implementadas.

Pueden existir muchas soluciones manuales, y todas son bienvenidas. Por ejemplo, puedo forzar que mis parciales se basen en IPartialViewModel <T> {prefijo de cadena pública; Modelo T; }. Pero prefiero alguna solución existente / aprobada.

ACTUALIZACIÓN: hay una pregunta similar sin respuesta aquí .

queen3
fuente

Respuestas:

110

Puede extender la clase auxiliar Html de esta manera:

using System.Web.Mvc.Html


 public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, string partialViewName)
    {
        string name = ExpressionHelper.GetExpressionText(expression);
        object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
        var viewData = new ViewDataDictionary(helper.ViewData)
        {
            TemplateInfo = new System.Web.Mvc.TemplateInfo
            {
                HtmlFieldPrefix = name
            }
        };

        return helper.Partial(partialViewName, model, viewData);

    }

y simplemente úselo en sus vistas de esta manera:

<%= Html.PartialFor(model => model.Child, "_AnotherViewModelControl") %>

y verás que todo está bien!

Mahmoud Moravej
fuente
17
Esto será incorrecto para la representación parcial anidada. Debe agregar el nuevo prefijo al prefijo anterior helper.ViewData.TemplateInfo.HtmlFieldPrefixen la forma de{oldprefix}.{newprefix}
Ivan Zlatev
@Mahmoud Su código funciona muy bien, pero descubrí que ViewData / ViewBag estaba vacío cuando llegó el momento de ejecutar el código en Partial. Descubrí que el ayudante, de tipo HtmlHelper <TModel> tenía una nueva propiedad ViewData, que ocultaba el modelo base. Con eso, reemplacé new ViewDataDictionary(helper.ViewData)con new ViewDataDictionary(((HtmlHelper)helper).ViewData). ¿Ves algún problema con eso?
Pat Newell
@IvanZlatev Gracias. Corregiré la publicación después de probarla.
Mahmoud Moravej
2
@IvanZlatev tiene razón. Antes de establecer el nombre, debe hacerlostring oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name;
kennethc
2
Una solución fácil para las plantillas anidadas es usar HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName (nombre)
Aman Mahajan
95

Hasta ahora, estaba buscando lo mismo que encontré en esta publicación reciente:

http://davybrion.com/blog/2011/01/prefixing-input-elements-of-partial-views-with-asp-net-mvc/

<% Html.RenderPartial("AnotherViewModelControl", Model.Child, new ViewDataDictionary
{
    TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Child1" }
})
%>
Jokin
fuente
3
Gracias por el enlace, esta es, con mucho, la mejor opción que se muestra aquí
BZ
32
Muy tarde para esta fiesta, pero si hace este enfoque, debe usar el ViewDataDictionaryconstructor que toma la corriente ViewData, o perderá errores de estado del modelo, datos de validación, etc.
bhamlin
9
basándose en el comentario de Bhamlin. también puede pasar un prefijo anidado como (sry esto es vb.net ex): Html.RenderPartial ("AnotherViewModelControl", Model.PropX, New ViewDataDictionary (ViewData) With {.TemplateInfo = New TemplateInfo () With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}})
hubson bropa
1
Gran respuesta, +1. Quizás valdría la pena editarlo para tener en cuenta el comentario de
@bhamlin
1
Tenga en cuenta que si necesita devolver este parcial desde un controlador, también deberá establecer este prefijo allí. stackoverflow.com/questions/6617768/…
The Muffin Man
12

Mi respuesta, basada en la respuesta de Mahmoud Moravej, incluido el comentario de Ivan Zlatev.

    public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, string partialViewName)
    {
            string name = ExpressionHelper.GetExpressionText(expression);
            object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
            StringBuilder htmlFieldPrefix = new StringBuilder();
            if (helper.ViewData.TemplateInfo.HtmlFieldPrefix != "")
            {
                htmlFieldPrefix.Append(helper.ViewData.TemplateInfo.HtmlFieldPrefix);
                htmlFieldPrefix.Append(name == "" ? "" : "." + name);
            }
            else
                htmlFieldPrefix.Append(name);

            var viewData = new ViewDataDictionary(helper.ViewData)
            {
                TemplateInfo = new System.Web.Mvc.TemplateInfo
                {
                    HtmlFieldPrefix = htmlFieldPrefix.ToString()
                }
            };

        return helper.Partial(partialViewName, model, viewData);
    }

Editar: la respuesta de Mohamoud es incorrecta para la representación parcial anidada. Debe agregar el nuevo prefijo al prefijo anterior, solo si es necesario. Esto no quedó claro en las últimas respuestas (:

asoifer1879
fuente
6
Sería útil para otros si explicara cómo lo anterior mejora la respuesta existente.
Leigh
2
Después de un error de copiar y pegar con dedos gordos, este funcionó perfectamente para ASP.NET MVC 5. +1.
Greg Burghardt
9

Usando MVC2 puedes lograr esto.

Aquí está la vista fuertemente tipada:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcLearner.Models.Person>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Create</h2>

    <% using (Html.BeginForm()) { %>
        <%= Html.LabelFor(person => person.Name) %><br />
        <%= Html.EditorFor(person => person.Name) %><br />
        <%= Html.LabelFor(person => person.Age) %><br />
        <%= Html.EditorFor(person => person.Age) %><br />
        <% foreach (String FavoriteFoods in Model.FavoriteFoods) { %>
            <%= Html.LabelFor(food => FavoriteFoods) %><br />
            <%= Html.EditorFor(food => FavoriteFoods)%><br />
        <% } %>
        <%= Html.EditorFor(person => person.Birthday, "TwoPart") %>
        <input type="submit" value="Submit" />
    <% } %>

</asp:Content>

Aquí está la vista fuertemente tipada para la clase secundaria (que debe almacenarse en una subcarpeta del directorio de vistas llamada EditorTemplates):

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

<%= Html.LabelFor(birthday => birthday.Day) %><br />
<%= Html.EditorFor(birthday => birthday.Day) %><br />

<%= Html.LabelFor(birthday => birthday.Month) %><br />
<%= Html.EditorFor(birthday => birthday.Month) %><br />

Aquí está el controlador:

public class PersonController : Controller
{
    //
    // GET: /Person/
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Index()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Create()
    {
        Person person = new Person();
        person.FavoriteFoods.Add("Sushi");
        return View(person);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Person person)
    {
        return View(person);
    }
}

Aquí están las clases personalizadas:

public class Person
{
    public String Name { get; set; }
    public Int32 Age { get; set; }
    public List<String> FavoriteFoods { get; set; }
    public TwoPart Birthday { get; set; }

    public Person()
    {
        this.FavoriteFoods = new List<String>();
        this.Birthday = new TwoPart();
    }
}

public class TwoPart
{
    public Int32 Day { get; set; }
    public Int32 Month { get; set; }
}

Y la fuente de salida:

<form action="/Person/Create" method="post"><label for="Name">Name</label><br /> 
    <input class="text-box single-line" id="Name" name="Name" type="text" value="" /><br /> 
    <label for="Age">Age</label><br /> 
    <input class="text-box single-line" id="Age" name="Age" type="text" value="0" /><br /> 
    <label for="FavoriteFoods">FavoriteFoods</label><br /> 
    <input class="text-box single-line" id="FavoriteFoods" name="FavoriteFoods" type="text" value="Sushi" /><br /> 
    <label for="Birthday_Day">Day</label><br /> 
    <input class="text-box single-line" id="Birthday_Day" name="Birthday.Day" type="text" value="0" /><br /> 

    <label for="Birthday_Month">Month</label><br /> 
    <input class="text-box single-line" id="Birthday_Month" name="Birthday.Month" type="text" value="0" /><br /> 
    <input type="submit" value="Submit" /> 
</form>

Ahora esto está completo. Establezca un punto de interrupción en la acción Crear controlador de publicación para verificar. Sin embargo, no use esto con listas porque no funcionará. Vea mi pregunta sobre el uso de EditorTemplates con IEnumerable para obtener más información al respecto.

Nick Larsen
fuente
Sí, lo parece. El problema es que las listas no funcionan (mientras que los modelos de vista anidada suelen estar en listas) y que es v2 ... que no estoy listo para usar en producción. Pero aún así es bueno saber que será algo que necesito ... cuando llegue (así que +1).
queen3
También me encontré con esto el otro día, matthidinger.com/archive/2009/08/15/… , es posible que desee ver si puede descubrir cómo extenderlo para los editores (aunque todavía en MVC2). Pasé unos minutos en eso, pero seguí teniendo problemas porque todavía no estoy a la altura de las expresiones. Quizás puedas hacerlo mejor que yo.
Nick Larsen
1
Un enlace útil, gracias. No es que crea que es posible hacer que EditorFor funcione aquí, porque supongo que no generará [0] índices (apuesto a que todavía no lo admite). Una solución sería convertir la salida de EditorFor () en una cadena y modificarla manualmente (añadir los prefijos necesarios). Sin embargo, un truco sucio. ¡Hm! Puedo hacer el método de extensión para Html.Helper (). UsePrefix () que simplemente reemplazará name = "x" con name = "prefix.x" ... en MVC v1. Todavía es un poco de trabajo, pero no tanto. Y Html.WithPrefix ("prefix"). RenderPartial () que funciona en pareja.
queen3
+1 por recomendar el Html.EditorFormétodo para representar el formulario secundario. Esto es precisamente para lo que se supone que se usan las plantillas de editor. Esta debería ser la respuesta.
Greg Burghardt
9

Esta es una pregunta antigua, pero para cualquiera que llegue aquí en busca de una solución, considere usarla EditorFor, como se sugiere en un comentario en https://stackoverflow.com/a/29809907/456456 . Para pasar de una vista parcial a una plantilla de editor, siga estos pasos.

  1. Verifique que su vista parcial esté vinculada a ComplexType .

  2. Mueva su vista parcial a una subcarpeta EditorTemplates de la carpeta de la vista actual, o a la carpeta Shared . Ahora es una plantilla de editor.

  3. Cambiar @Html.Partial("_PartialViewName", Model.ComplexType)a @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName"). La plantilla del editor es opcional si es la única plantilla para el tipo complejo.

Los elementos de entrada HTML se nombrarán automáticamente ComplexType.Fieldname.

R. Schreurs
fuente
8

PartailFor para asp.net Core 2 en caso de que alguien lo necesite.

    public static ModelExplorer GetModelExplorer<TModel, TResult>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TResult>> expression)
    {
        if (expression == null)
            throw new ArgumentNullException(nameof(expression));
        return ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
    }

    public static IHtmlContent PartialFor<TModel, TResult>(this IHtmlHelper<TModel> helper, Expression<Func<TModel, TResult>> expression, string partialViewName, string prefix = "")
    {
        var modelExplorer = helper.GetModelExplorer(expression);
        var viewData = new ViewDataDictionary(helper.ViewData);
        viewData.TemplateInfo.HtmlFieldPrefix += prefix;
        return helper.Partial(partialViewName, modelExplorer.Model, viewData);
    }
Rahma Samaroon
fuente
3

También encontré este problema y, después de mucho dolor, descubrí que era más fácil rediseñar mis interfaces de manera que no necesitaba publicar objetos de modelo anidados. Esto me obligó a cambiar los flujos de trabajo de mi interfaz: seguro que ahora requiero que el usuario haga en dos pasos lo que soñé hacer en uno, pero la facilidad de uso y la capacidad de mantenimiento del código del nuevo enfoque es de mayor valor para mí ahora.

Espero que esto ayude a algunos.

Matt Kocaj
fuente
1

Puede agregar un ayudante para RenderPartial que toma el prefijo y lo coloca en ViewData.

    public static void RenderPartial(this HtmlHelper helper,string partialViewName, object model, string prefix)
    {
        helper.ViewData["__prefix"] = prefix;
        helper.RenderPartial(partialViewName, model);
    }

Luego, un ayudante adicional que concatena el valor ViewData

    public static void GetName(this HtmlHelper helper, string name)
    {
        return string.Concat(helper.ViewData["__prefix"], name);
    }

y así en la vista ...

<% Html.RenderPartial("AnotherViewModelControl", Model.Child, "Child.") %>

en el parcial ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>
Anthony Johnston
fuente
Sí, eso es lo que describí en un comentario a stackoverflow.com/questions/1488890/… . Una solución aún mejor sería RenderPartial, que también utiliza lambda, que es fácil de implementar. Aún así, gracias por el código, supongo que ese es el enfoque más indoloro y atemporal.
queen3
1
¿Por qué no usar helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix? Lo estoy usando en mi ayudante y parece estar funcionando bien.
ajbeaven
0

Como usted, agrego la propiedad Prefix (una cadena) a mis ViewModels que agrego antes de los nombres de entrada de mi modelo. (YAGNI previene lo siguiente)

Una solución más elegante podría ser un modelo de vista base que tenga esta propiedad y algunos HtmlHelpers que verifiquen si el modelo de vista se deriva de esta base y, de ser así, agregue el prefijo al nombre de entrada.

Espero que ayude,

Dan

Daniel Elliott
fuente
1
Hmm, muy mal. Puedo imaginar muchas soluciones elegantes, con HtmlHelpers personalizados que verifican propiedades, atributos, renderizado personalizado, etc. Pero, por ejemplo, si uso MvcContrib FluentHtml, ¿las reescribo todas para admitir mis hacks? Es extraño que nadie hable de eso, como si todo el mundo solo usara ViewModels planos de un solo nivel ...
queen3
De hecho, después de usar el marco durante un período de tiempo y esforzarse por obtener un código de vista limpio y agradable, creo que el ViewModel escalonado es inevitable. El Basket ViewModel dentro del Order ViewModel, por ejemplo.
Daniel Elliott
0

¿Qué tal antes de llamar a RenderPartial?

<% ViewData["Prefix"] = "Child."; %>
<% Html.RenderPartial("AnotherViewModelControl", Model.Child) %>

Entonces en tu parcial tienes

<%= Html.TextBox(ViewData["Prefix"] + "Name", Model.Name) %>
Anthony Johnston
fuente
1
Esto es básicamente lo mismo que pasarlo manualmente (es seguro derivar todos los modelos de vista de IViewModel con "IViewModel SetPrefix (string)" en su lugar), y es muy feo, a menos que reescriba todos los ayudantes Html y RenderPartial para que se administren automáticamente esta. El problema no es cómo hacerlo, esto ya lo resolví; el problema es que se puede hacer automáticamente.
queen3
como no hay un lugar común en el que pueda poner código que afectará a todos los ayudantes html, no podrá hacerlo automáticamente. ver otra respuesta ...
Anthony Johnston