ASP.NET MVC Częściowe widoki: przedrostki nazw danych wejściowych

120

Załóżmy, że mam ViewModel jak

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

W widoku mogę renderować częściowe za pomocą

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

W części zrobię

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

Jednak problem polega na tym, że oba będą renderować name = "Name", podczas gdy ja potrzebuję mieć name = "Child.Name", aby spinacz modelu działał poprawnie. Lub name = "Child2.Name", gdy renderuję drugą właściwość przy użyciu tego samego widoku częściowego.

Jak sprawić, by widok częściowy automatycznie rozpoznawał wymagany prefiks? Mogę podać to jako parametr, ale jest to zbyt niewygodne. Jest to jeszcze gorsze, gdy chcę, na przykład, renderować go rekurencyjnie. Czy istnieje sposób renderowania widoków częściowych z prefiksem, czy jeszcze lepiej, z automatyczną rekonstrukcją wywołującego wyrażenia lambda, tak aby

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

automatycznie doda poprawne „Dziecko”. prefiks do wygenerowanych ciągów nazwy / identyfikatora?

Mogę zaakceptować dowolne rozwiązanie, w tym zewnętrzne silniki i biblioteki widoku - faktycznie używam Spark View Engine („rozwiązuję” problem za pomocą jego makr) i MvcContrib, ale nie znalazłem tam rozwiązania. XForms, InputBuilder, MVC v2 - każde narzędzie / insight zapewniające tę funkcjonalność będzie świetne.

Obecnie sam myślę o zakodowaniu tego kodu, ale wydaje mi się, że to strata czasu, nie mogę uwierzyć, że ta trywialna rzecz nie jest już zaimplementowana.

Może istnieć wiele rozwiązań ręcznych i wszystkie z nich są mile widziane. Na przykład mogę zmusić moje części składowe, aby były oparte na IPartialViewModel <T> {public string Prefix; Model T; }. Ale wolałbym jakieś istniejące / zatwierdzone rozwiązanie.

UPDATE: tam podobne pytanie bez odpowiedzi tutaj .

królowa3
źródło

Odpowiedzi:

110

Możesz rozszerzyć klasę pomocniczą Html o to:

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

    }

i po prostu użyj go w swoich widokach w następujący sposób:

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

a zobaczysz, że wszystko jest w porządku!

Mahmoud Moravej
źródło
17
Będzie to nieprawidłowe w przypadku zagnieżdżonego renderowania częściowego. Musisz dodać nowy prefiks do starego przedrostka z helper.ViewData.TemplateInfo.HtmlFieldPrefixw postaci{oldprefix}.{newprefix}
Ivan Zlatev
@Mahmoud Twój kod działa świetnie, ale stwierdziłem, że ViewData / ViewBag jest pusty, kiedy nadszedł czas, aby wykonać kod w części. Okazało się, że pomocnik typu HtmlHelper <TModel> ma nową właściwość ViewData, która ukrywa model podstawowy. Z tym, że otrzymuje new ViewDataDictionary(helper.ViewData)się new ViewDataDictionary(((HtmlHelper)helper).ViewData). Czy widzisz z tym jakiś problem?
Pat Newell,
@IvanZlatev Thanks. Poprawię post po przetestowaniu go.
Mahmoud Moravej
2
@IvanZlatev jest poprawne. Zanim ustawisz nazwę, powinieneś to zrobićstring oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name;
kennethc
2
Łatwym rozwiązaniem dla zagnieżdżonych szablonów jest użycie HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName (name)
Aman Mahajan
95

do tej pory szukałem tego samego, co znalazłem w ostatnim wpisie:

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
źródło
3
Dzięki za link, jest to zdecydowanie najlepsza opcja wymieniona tutaj
BZ
32
Bardzo późno na tę imprezę, ale jeśli zrobisz to podejście, powinieneś użyć ViewDataDictionarykonstruktora, który pobiera prąd ViewData, albo stracisz błędy stanu modelu, dane walidacyjne itp.
bhamlin
9
opierając się na komentarzu bhamlin. możesz również przekazać zagnieżdżony prefiks, taki jak (sry to jest vb.net ex): Html.RenderPartial ("AnotherViewModelControl", Model.PropX, New ViewDataDictionary (ViewData) With {.TemplateInfo = New TemplateInfo () With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}})
hubson bropa
1
Świetna odpowiedź, +1. Może warto byłoby go edytować, aby uwzględnić komentarz
@bhamlin
1
Zwróć uwagę, że jeśli chcesz zwrócić tę część z kontrolera, musisz tam również ustawić ten prefiks. stackoverflow.com/questions/6617768/…
The Muffin Man
12

Moja odpowiedź na podstawie odpowiedzi Mahmouda Moraveja, w tym komentarza Ivana Zlateva.

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

Edycja: odpowiedź Mohamoud jest niepoprawna dla zagnieżdżonego renderowania częściowego. Musisz dodać nowy prefiks do starego przedrostka, tylko jeśli jest to konieczne. Nie było to jasne w ostatnich odpowiedziach (:

asoifer1879
źródło
6
Byłoby pomocne dla innych, gdybyś wyjaśnił, w jaki sposób powyższe poprawia się w stosunku do istniejącej odpowiedzi.
Leigh,
2
Po fatalnym błędzie kopiowania i wklejania ten działał idealnie dla ASP.NET MVC 5. +1.
Greg Burghardt
9

Dzięki MVC2 możesz to osiągnąć.

Oto widok silnie wpisany:

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

Oto widok silnie wpisany dla klasy podrzędnej (który musi być przechowywany w podfolderze katalogu widoku o nazwie 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 />

Oto kontroler:

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

Oto klasy niestandardowe:

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

I źródło wyjściowe:

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

Teraz to jest zakończone. Ustaw punkt przerwania w akcji kontrolera Utwórz post do weryfikacji. Nie używaj tego jednak z listami, ponieważ to nie zadziała. Zobacz moje pytanie dotyczące używania EditorTemplates z IEnumerable, aby uzyskać więcej informacji na ten temat.

Nick Larsen
źródło
Tak, na to wygląda. Problem polega na tym, że listy nie działają (podczas gdy modele widoków zagnieżdżonych są zwykle na listach) i że jest to wersja 2 ... której nie jestem jeszcze gotowy do użycia w produkcji. Ale nadal dobrze wiedzieć, że będzie to coś, czego potrzebuję ... kiedy nadejdzie (więc +1).
królowa 3
Niedawno natknąłem się na to, matthidinger.com/archive/2009/08/15/… , możesz chcieć zobaczyć, czy możesz dowiedzieć się, jak rozszerzyć to dla redaktorów (choć nadal w MVC2). Spędziłem nad tym kilka minut, ale wciąż napotykałem problemy, ponieważ nie jestem jeszcze na poziomie wyrażeń. Może możesz zrobić lepiej niż ja.
Nick Larsen
1
Przydatny link, dzięki. Nie znaczy to, że wydaje mi się, że można tutaj zrobić EditorFor, ponieważ przypuszczam, że nie wygeneruje [0] indeksów (założę się, że jeszcze go nie obsługuje). Jednym z rozwiązań byłoby wyrenderowanie wyjścia EditorFor () na łańcuch i ręczne dostrojenie wyniku (dołączenie wymaganych przedrostków). Ale brudny hack. Hm! Mogę zrobić metodę rozszerzenia dla Html.Helper (). UsePrefix (), która po prostu zastąpi name = "x" przez name = "prefix.x" ... w MVC v1. Wciąż trochę pracy, ale niewiele. Oraz Html.WithPrefix ("prefiks"). RenderPartial (), który działa w parze.
królowa 3
+1 za zalecenie Html.EditorFormetody renderowania formularza podrzędnego. Właśnie do tego mają służyć szablony edytorów. To powinna być odpowiedź.
Greg Burghardt,
9

To stare pytanie, ale dla każdego, kto przyjeżdża tutaj i szuka rozwiązania, rozważ użycie EditorFor, jak zasugerowano w komentarzu na https://stackoverflow.com/a/29809907/456456 . Aby przejść z częściowego widoku do szablonu edytora, wykonaj następujące kroki.

  1. Sprawdź, czy widok częściowy jest powiązany z typem ComplexType .

  2. Przenieś widok częściowy do podfolderu EditorTemplates w bieżącym folderze widoku lub do folderu Shared . Teraz jest to szablon edytora.

  3. Zmień @Html.Partial("_PartialViewName", Model.ComplexType)na @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName"). Szablon edytora jest opcjonalny, jeśli jest to jedyny szablon dla typu złożonego.

Elementy wejściowe HTML zostaną automatycznie nazwane ComplexType.Fieldname.

R. Schreurs
źródło
8

PartailFor dla asp.net Core 2 na wypadek, gdyby ktoś tego potrzebował.

    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
źródło
3

Natknąłem się również na ten problem i po wielu bólach stwierdziłem, że łatwiej było przeprojektować moje interfejsy tak, że nie musiałem ponownie publikować zagnieżdżonych obiektów modelu. Zmusiło mnie to do zmiany przepływów pracy w interfejsie: z pewnością wymagam teraz od użytkownika wykonania w dwóch krokach tego, o czym marzyłem, na jednym, ale użyteczność i łatwość utrzymania kodu nowego podejścia ma teraz dla mnie większą wartość.

Mam nadzieję, że to pomoże niektórym.

Matt Kocaj
źródło
1

Możesz dodać pomocnika dla RenderPartial, który pobiera prefiks i wstawia go do ViewData.

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

Następnie kolejny pomocnik, który konkatenuje wartość ViewData

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

i tak na widoku ...

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

w częściowym ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>
Anthony Johnston
źródło
Tak, to właśnie opisałem w komentarzu do stackoverflow.com/questions/1488890/… . Jeszcze lepszym rozwiązaniem byłby RenderPartial, który również pobiera lambdę, co jest łatwe do zaimplementowania. Mimo to, dzięki za kod, myślę, że to najbardziej bezbolesne i ponadczasowe podejście.
królowa 3
1
dlaczego nie użyć helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix? Używam tego w moim pomocniku i wydaje się, że działa dobrze.
ajbeaven
0

Podobnie jak Ty, dodaję właściwość Prefix (ciąg) do moich ViewModels, które dołączam przed nazwami wejściowymi powiązanymi z modelem. (YAGNI zapobiega poniższym)

Bardziej eleganckim rozwiązaniem może być model widoku podstawowego, który ma tę właściwość i niektóre HtmlHelpers, które sprawdzają, czy model widoku pochodzi z tej bazy, a jeśli tak, dołącz prefiks do nazwy wejściowej.

Mam nadzieję, że to pomoże

Dan

Daniel Elliott
źródło
1
Hmm, szkoda. Mogę sobie wyobrazić wiele eleganckich rozwiązań, z niestandardowymi HtmlHelpersami sprawdzającymi właściwości, atrybuty, niestandardowe renderowanie itp. Ale na przykład, jeśli używam MvcContrib FluentHtml, czy mogę przepisać je wszystkie, aby obsługiwały moje hacki? Dziwne, że nikt o tym nie mówi, jakby wszyscy
używali płaskich jednopoziomowych modeli
Rzeczywiście, po korzystaniu z frameworka przez dowolny czas i dążeniu do ładnego, czystego kodu widoku, myślę, że warstwowy ViewModel jest nieunikniony. Na przykład model widoku koszyka w ramach modelu widoku zamówienia.
Daniel Elliott
0

Może tuż przed wywołaniem RenderPartial

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

Więc w swojej części masz

<%= Html.TextBox(ViewData["Prefix"] + "Name", Model.Name) %>
Anthony Johnston
źródło
1
Jest to w zasadzie to samo, co przekazanie go ręcznie (można bezpiecznie wyprowadzić wszystkie modele widoku z IViewModel z "IViewModel SetPrefix (string)") i jest bardzo brzydkie, chyba że przepiszę wszystkie pomocniki Html i RenderPartial, aby automatycznie zarządzały to. Problem nie polega na tym, jak to zrobić, już to rozwiązałem; problem polega na tym, czy można to zrobić automatycznie.
królowa 3
ponieważ nie ma zwykłego miejsca, w którym można umieścić kod, który wpłynie na wszystkich pomocników HTML, nie byłbyś w stanie zrobić tego automatycznie. zobacz inną odpowiedź ...
Anthony Johnston,