Exibições parciais do ASP.NET MVC: prefixos de nome de entrada

120

Suponha que eu tenha o 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; }
}

Na visão, posso renderizar parcial com

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

No parcial eu vou fazer

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

No entanto, o problema é que ambos renderizarão name = "Name" enquanto eu preciso ter name = "Child.Name" para que o fichário do modelo funcione corretamente. Ou, name = "Child2.Name" quando eu renderizar a segunda propriedade usando a mesma exibição parcial.

Como faço para que minha visualização parcial reconheça automaticamente o prefixo necessário? Eu posso passar isso como um parâmetro, mas isso é muito inconveniente. Isso é ainda pior quando eu quero, por exemplo, renderizá-lo recursivamente. Existe uma maneira de renderizar visões parciais com um prefixo ou, melhor ainda, com a reconciliação automática da expressão lambda de chamada para que

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

adicionará automaticamente "Filho" correto. prefixo para as cadeias de nome / ID geradas?

Posso aceitar qualquer solução, incluindo mecanismos e bibliotecas de exibição de terceiros - eu realmente uso o Spark View Engine (eu "resolvo" o problema usando suas macros) e o MvcContrib, mas não encontrei uma solução lá. XForms, InputBuilder, MVC v2 - qualquer ferramenta / insight que forneça essa funcionalidade será excelente.

Atualmente, penso em codificar isso sozinho, mas parece uma perda de tempo, não acredito que esse material trivial ainda não esteja implementado.

Muitas soluções manuais podem existir e todas são bem-vindas. Por exemplo, eu posso forçar minhas parciais a serem baseadas em IPartialViewModel <T> {public string Prefix; Modelo T; } Mas prefiro uma solução existente / aprovada.

ATUALIZAÇÃO: há uma pergunta semelhante sem resposta aqui .

queen3
fonte

Respostas:

110

Você pode estender a classe auxiliar Html da seguinte maneira:

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

    }

e simplesmente use-o em seus pontos de vista como este:

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

e você verá que está tudo bem!

Mahmoud Moravej
fonte
17
Isso estará incorreto para a renderização parcial aninhada. Você precisa anexar o novo prefixo ao prefixo antigo helper.ViewData.TemplateInfo.HtmlFieldPrefixno formato de{oldprefix}.{newprefix}
Ivan Zlatev 1/12/12
@Mahmoud Seu código funciona muito bem, mas eu estava achando que o ViewData / ViewBag estava vazio quando chegou a hora de executar o código no Partial. Descobri que o auxiliar, do tipo HtmlHelper <TModel> tinha uma nova propriedade ViewData, que ocultava o modelo base. Com isso, substituí new ViewDataDictionary(helper.ViewData)por new ViewDataDictionary(((HtmlHelper)helper).ViewData). Você vê algum problema com isso?
Pat Newell
@IvanZlatev Thanks. Corrigirei a postagem após testá-la.
Mahmoud Moravej
2
@IvanZlatev está correto. Antes de definir o nome, você deve fazerstring oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name;
kennethc
2
Uma correção fácil para modelos aninhados é usar HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName (name)
Aman Mahajan
95

Até agora, eu estava procurando a mesma coisa que encontrei neste post recente:

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
fonte
3
Obrigado pelo link, esta é de longe a melhor opção listada aqui
BZ
32
Super tarde para essa festa, mas se você fizer essa abordagem, você deve usar o ViewDataDictionaryconstrutor que leva a corrente ViewData, ou você vai perder erros modelo de estado, dados de validação, etc.
bhamlin
9
com base no comentário de bhamlin. você também pode passar o prefixo aninhado, como (sry, isto é vb.net ex): Html.RenderPartial ("AnotherViewModelControl", Model.PropX, New ViewDataDictionary (ViewData) With {.TemplateInfo = New TemplateInfo () With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}})
hubson bropa
1
Ótima resposta, +1. Talvez valeria a pena editá-lo a tomar @bhamlin comentário em conta
ken2k
1
Observe que, se você precisar retornar essa parcial de um controlador, também precisará definir esse prefixo. stackoverflow.com/questions/6617768/…
The Muffin Man
12

Minha resposta, baseada na resposta de Mahmoud Moravej, incluindo o comentário 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);
    }

Edit: A resposta do Mohamoud está incorreta para renderização parcial aninhada. Você precisa anexar o novo prefixo ao prefixo antigo, apenas se for necessário. Isso não ficou claro nas respostas mais recentes (:

asoifer1879
fonte
6
Seria útil para outras pessoas se você especificar como o que foi dito acima melhora a resposta existente.
Leigh
2
Após um erro de copiar e colar, este funcionou perfeitamente para o ASP.NET MVC 5. +1.
Greg Burghardt
9

Usando o MVC2, você pode conseguir isso.

Aqui está a visão fortemente 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>

Aqui está a exibição fortemente tipada para a classe filho (que deve ser armazenada em uma subpasta do diretório de exibição chamada 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 />

Aqui está o 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);
    }
}

Aqui estão as classes 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; }
}

E a fonte de saída:

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

Agora isso está completo. Defina um ponto de interrupção na ação Criar controlador de postagem para verificar. No entanto, não use isso com listas, porque não funcionará. Veja minha pergunta sobre o uso de EditorTemplates com IEnumerable para obter mais informações.

Nick Larsen
fonte
Sim, parece que sim. Os problemas são que as listas não funcionam (enquanto os modelos de exibição aninhados geralmente estão nas listas) e que é a v2 ... que não estou pronta para usar na produção. Mas ainda é bom saber que será algo que eu preciso ... quando se trata (então +1).
31 de
Eu também me deparei com isso outro dia, matthidinger.com/archive/2009/08/15/… . Você pode querer descobrir se consegue descobrir como estendê-lo para editores (embora ainda no MVC2). Passei alguns minutos nisso, mas continuei tendo problemas porque ainda não sou parecido com as expressões. Talvez você possa fazer melhor do que eu.
31412 Nick Larsen
1
Um link útil, obrigado. Não que eu ache que é possível fazer o EditorFor funcionar aqui, porque não gerará [0] índices, suponho (aposto que ainda não o suporta). Uma solução seria renderizar a saída EditorFor () para string e ajustar manualmente a saída (acrescente prefixos necessários). Um truque sujo, no entanto. Hum! Eu posso fazer o método de extensão para Html.Helper (). UsePrefix (), que substituirá apenas name = "x" por name = "prefix.x" ... no MVC v1. Ainda um pouco de trabalho, mas não tanto. E Html.WithPrefix ("prefix"). RenderPartial () que funciona em par.
31 de
+1 por recomendar o Html.EditorFormétodo para renderizar o formulário filho. É exatamente para isso que os modelos de editor devem ser usados. Essa deve ser a resposta.
Greg Burghardt
9

Essa é uma pergunta antiga, mas para quem chega aqui procurando uma solução, considere usar EditorFor, conforme sugerido em um comentário em https://stackoverflow.com/a/29809907/456456 . Para passar de uma vista parcial para um modelo de editor, siga estas etapas.

  1. Verifique se sua visualização parcial está vinculada ao ComplexType .

  2. Mova sua exibição parcial para uma subpasta EditorTemplates da pasta de exibição atual ou para a pasta Shared . Agora, é um modelo de editor.

  3. Mude @Html.Partial("_PartialViewName", Model.ComplexType)para @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName"). O modelo do editor é opcional se for o único modelo para o tipo complexo.

Os elementos de entrada HTML serão nomeados automaticamente ComplexType.Fieldname.

R. Schreurs
fonte
8

PartailFor para asp.net Core 2, caso alguém precise.

    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
fonte
3

Também deparei com esse problema e, depois de muita dor, achei mais fácil redesenhar minhas interfaces, de modo que não precisei postar novamente objetos de modelo aninhados. Isso me forçou a alterar meus fluxos de trabalho de interface: agora eu exijo que o usuário execute em duas etapas o que eu sonhava em uma, mas a usabilidade e a manutenção do código da nova abordagem são de maior valor para mim agora.

Espero que isso ajude alguns.

Matt Kocaj
fonte
1

Você pode adicionar um auxiliar para o RenderPartial que pega o prefixo e o exibe no ViewData.

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

Em seguida, outro auxiliar que concatena o valor ViewData

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

e assim na visão ...

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

no parcial ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>
Anthony Johnston
fonte
Sim, foi o que descrevi em um comentário para stackoverflow.com/questions/1488890/… . Uma solução ainda melhor seria RenderPartial, que também usa lambda, que é fácil de implementar. Ainda assim, obrigado pelo código, acho que é a abordagem mais indolor e atemporal.
queen3
1
por que não usar helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix? Estou usando isso no meu ajudante e parece estar funcionando bem.
28411 ajbeaven
0

Como você, adiciono a propriedade Prefix (uma string) aos meus ViewModels, os quais anexo antes dos nomes de entrada associados ao meu modelo. (YAGNI impedindo o abaixo)

Uma solução mais elegante pode ser um modelo de vista de base que tenha essa propriedade e alguns HtmlHelpers que verificam se o modelo de vista deriva dessa base e, em caso afirmativo, anexa o prefixo ao nome da entrada.

Espero que ajude,

Dan

Daniel Elliott
fonte
1
Hum, que pena. Posso imaginar muitas soluções elegantes, com HtmlHelpers personalizados verificando propriedades, atributos, renderização personalizada, etc ... Mas, por exemplo, se eu usar o MvcContrib FluentHtml, reescrevo todos eles para dar suporte aos meus hacks? É estranho que ninguém fala sobre isso, como se todo mundo usa apenas ViewModels de nível único planas ...
queen3
De fato, depois de usar a estrutura por qualquer período de tempo e buscar um bom código de visualização limpo, acho que o ViewModel em camadas é inevitável. O Basket ViewModel no Order ViewModel, por exemplo.
31511 Daniel Elliott
0

Que tal antes de ligar para RenderPartial, você

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

Então no seu parcial você tem

<%= Html.TextBox(ViewData["Prefix"] + "Name", Model.Name) %>
Anthony Johnston
fonte
1
Isso é basicamente o mesmo que passá-lo manualmente (é seguro derivar todos os modelos de exibição de IViewModel com "IViewModel SetPrefix (string)") e é muito feio, a menos que eu reescreva todos os auxiliares de HTML e RenderPartial para que eles gerenciem automaticamente isto. O problema não é como fazê-lo, já resolvi isso; o problema é: isso pode ser feito automaticamente.
31 de
Como não há um local comum, você pode colocar um código que afetará todos os auxiliares html. Você não conseguiria fazer isso automaticamente. Veja outra resposta ...
Anthony Johnston