Vues partielles ASP.NET MVC: préfixes de nom d'entrée

120

Supposons que j'ai ViewModel comme

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

Dans la vue, je peux rendre un partiel avec

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

Dans le partiel je ferai

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

Cependant, le problème est que les deux rendront name = "Name" alors que j'ai besoin d'avoir name = "Child.Name" pour que le classeur de modèle fonctionne correctement. Ou, name = "Child2.Name" lorsque je rend la deuxième propriété en utilisant la même vue partielle.

Comment faire pour que ma vue partielle reconnaisse automatiquement le préfixe requis? Je peux le passer en paramètre mais c'est trop gênant. C'est encore pire quand je veux par exemple le rendre récursif. Existe-t-il un moyen de rendre des vues partielles avec un préfixe, ou, mieux encore, avec une reconition automatique de l'expression lambda appelante de sorte que

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

ajoutera automatiquement un "enfant" correct. préfixe aux chaînes de nom / id générées?

Je peux accepter n'importe quelle solution, y compris les moteurs de visualisation tiers et les bibliothèques - j'utilise en fait Spark View Engine (je «résous» le problème en utilisant ses macros) et MvcContrib, mais je n'y ai pas trouvé de solution. XForms, InputBuilder, MVC v2 - tout outil / aperçu qui fournit cette fonctionnalité sera génial.

Actuellement, je pense à le coder moi-même, mais cela semble être une perte de temps, je ne peux pas croire que ce truc trivial ne soit pas déjà implémenté.

De nombreuses solutions manuelles peuvent exister et toutes sont les bienvenues. Par exemple, je peux forcer mes partiels à être basés sur IPartialViewModel <T> {public string Prefix; Modèle T; }. Mais je préfère une solution existante / approuvée.

MISE À JOUR: il y a une question similaire sans réponse ici .

reine3
la source

Réponses:

110

Vous pouvez étendre la classe d'assistance Html par ceci:

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

    }

et utilisez-le simplement dans vos vues comme ceci:

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

et vous verrez que tout va bien!

Mahmoud Moravej
la source
17
Ce sera incorrect pour le rendu partiel imbriqué. Vous devez ajouter le nouveau préfixe à l'ancien préfixe de helper.ViewData.TemplateInfo.HtmlFieldPrefixsous la forme{oldprefix}.{newprefix}
Ivan Zlatev
@Mahmoud Votre code fonctionne très bien, mais je trouvais que le ViewData / ViewBag était vide au moment d'exécuter du code dans le Partial. J'ai trouvé que l'assistant, de type HtmlHelper <TModel> avait une nouvelle propriété ViewData, qui cachait le modèle de base. Avec ça, j'ai remplacé new ViewDataDictionary(helper.ViewData)par new ViewDataDictionary(((HtmlHelper)helper).ViewData). Voyez-vous un problème avec cela?
Pat Newell
@IvanZlatev Merci. Je corrigerai le message après l'avoir testé.
Mahmoud Moravej
2
@IvanZlatev est correct. Avant de définir le nom, vous devriez le fairestring oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name;
kennethc
2
Une solution simple pour les modèles imbriqués consiste à utiliser HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName (nom)
Aman Mahajan
95

jusqu'à présent, je cherchais la même chose que j'ai trouvé dans ce post récent:

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
la source
3
Merci pour le lien, c'est de loin la meilleure option répertoriée ici
BZ
32
Super tard pour cette partie, mais si vous faites cette approche, vous devriez utiliser le ViewDataDictionaryconstructeur qui prend le courant ViewData, sinon vous perdrez les erreurs d'état du modèle, les données de validation, etc.
bhamlin
9
s'appuyant sur le commentaire de bhamlin. vous pouvez également transmettre un préfixe imbriqué tel que (sry c'est vb.net ex): Html.RenderPartial ("AnotherViewModelControl", Model.PropX, New ViewDataDictionary (ViewData) With {.TemplateInfo = New TemplateInfo () With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}})
hubson bropa
1
Excellente réponse, +1. Peut-être qu'il vaudrait la peine de le modifier pour prendre en compte le commentaire de
@bhamlin
1
Notez que si vous devez renvoyer ce partiel à partir d'un contrôleur, vous devrez également y définir ce préfixe. stackoverflow.com/questions/6617768/…
The Muffin Man
12

Ma réponse, basée sur la réponse de Mahmoud Moravej y compris le commentaire d'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: La réponse de Mohamoud est incorrecte pour le rendu partiel imbriqué. Vous devez ajouter le nouveau préfixe à l'ancien préfixe, uniquement si cela est nécessaire. Ce n'était pas clair dans les dernières réponses (:

asoifer1879
la source
6
Il serait utile aux autres d'expliquer comment ce qui précède améliore la réponse existante.
Leigh
2
Après une grosse erreur de copier-coller, celle-ci fonctionnait parfaitement pour ASP.NET MVC 5. +1.
Greg Burghardt
9

En utilisant MVC2, vous pouvez y parvenir.

Voici la vue fortement typée:

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

Voici la vue fortement typée pour la classe enfant (qui doit être stockée dans un sous-dossier du répertoire de vue appelé 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 />

Voici le contrôleur:

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

Voici les classes personnalisées:

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

Et la source de sortie:

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

Maintenant, c'est terminé. Définissez un point d'arrêt dans l'action Créer un contrôleur post à vérifier. Ne l'utilisez pas avec des listes car cela ne fonctionnera pas. Voir ma question sur l'utilisation de EditorTemplates avec IEnumerable pour plus d'informations à ce sujet.

Nick Larsen
la source
Oui, ça y ressemble. Le problème est que les listes ne fonctionnent pas (alors que les modèles de vues imbriquées sont généralement dans des listes) et que c'est v2 ... que je ne suis pas tout à fait prêt à utiliser en production. Mais toujours bon de savoir que ce sera quelque chose dont j'ai besoin ... quand ça viendra (donc +1).
queen3
Je suis également tombé sur cela l'autre jour, matthidinger.com/archive/2009/08/15/… , vous voudrez peut-être voir si vous pouvez trouver comment l'étendre pour les éditeurs (bien que toujours dans MVC2). J'ai passé quelques minutes dessus, mais j'ai continué à rencontrer des problèmes car je ne suis pas encore à la hauteur des expressions. Peut-être que tu peux faire mieux que moi.
Nick Larsen
1
Un lien utile, merci. Non pas que je pense qu'il soit possible de faire fonctionner EditorFor ici, car il ne générera pas d'index [0] je suppose (je parie qu'il ne le supporte pas encore du tout). Une solution serait de rendre la sortie EditorFor () en chaîne et d'ajuster manuellement la sortie (ajouter les préfixes requis). Un sale hack, cependant. Hm! Je peux faire une méthode d'extension pour Html.Helper (). UsePrefix () qui remplacera simplement name = "x" par name = "prefix.x" ... dans MVC v1. Encore un peu de travail mais pas tant que ça. Et Html.WithPrefix ("prefix"). RenderPartial () qui fonctionne par paire.
queen3
+1 pour avoir recommandé la Html.EditorForméthode de rendu du formulaire enfant. C'est précisément pour cela que les modèles d'éditeur sont censés être utilisés. Cela devrait être la réponse.
Greg Burghardt
9

C'est une vieille question, mais pour quiconque arrive ici à la recherche d'une solution, pensez à l'utiliser EditorFor, comme suggéré dans un commentaire sur https://stackoverflow.com/a/29809907/456456 . Pour passer d'une vue partielle à un modèle d'éditeur, procédez comme suit.

  1. Vérifiez que votre vue partielle est liée à ComplexType .

  2. Déplacez votre vue partielle vers un sous-dossier EditorTemplates du dossier de vue actuel ou vers le dossier Shared . Maintenant, c'est un modèle d'éditeur.

  3. Changer @Html.Partial("_PartialViewName", Model.ComplexType)en @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName"). Le modèle d'éditeur est facultatif s'il s'agit du seul modèle pour le type complexe.

Les éléments d'entrée Html seront automatiquement nommés ComplexType.Fieldname.

R. Schreurs
la source
8

PartailFor pour asp.net Core 2 au cas où quelqu'un en aurait besoin.

    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
la source
3

Je suis également tombé sur ce problème et après beaucoup de douleur, j'ai trouvé qu'il était plus facile de repenser mes interfaces de sorte que je n'ai pas besoin de publier des objets de modèle imbriqués. Cela m'a forcé à changer mes flux de travail d'interface: bien sûr, je demande maintenant à l'utilisateur de faire en deux étapes ce que je rêvais de faire sur une, mais la facilité d'utilisation et la maintenabilité du code de la nouvelle approche sont plus importantes pour moi maintenant.

J'espère que cela aide certains.

Matt Kocaj
la source
1

Vous pouvez ajouter un assistant pour le RenderPartial qui prend le préfixe et le fait apparaître dans ViewData.

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

Puis un autre assistant qui concatène la valeur ViewData

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

et donc dans la vue ...

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

dans le partiel ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>
Anthony Johnston
la source
Oui, c'est ce que j'ai décrit dans un commentaire sur stackoverflow.com/questions/1488890/… . Une solution encore meilleure serait RenderPartial qui prend également lambda, ce qui est facile à implémenter. Pourtant, merci pour le code, je suppose que c'est l'approche la plus indolore et la plus intemporelle.
queen3
1
pourquoi ne pas utiliser helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix? J'utilise cela dans mon assistant et cela semble fonctionner correctement.
ajbeaven
0

Comme vous, j'ajoute la propriété Prefix (une chaîne) à mes ViewModels que j'ajoute avant les noms d'entrée liés à mon modèle. (YAGNI empêchant le dessous)

Une solution plus élégante pourrait être un modèle de vue de base qui possède cette propriété et des HtmlHelpers qui vérifient si le modèle de vue dérive de cette base et, le cas échéant, ajoutent le préfixe au nom d'entrée.

J'espère que cela pourra aider,

Dan

Daniel Elliott
la source
1
Hmm, dommage. Je peux imaginer de nombreuses solutions élégantes, avec des HtmlHelpers personnalisés vérifiant les propriétés, les attributs, le rendu personnalisé, etc. Mais par exemple, si j'utilise MvcContrib FluentHtml, est-ce que je les réécris toutes pour prendre en charge mes hacks? C'est étrange que personne n'en parle, comme si tout le monde utilisait juste des ViewModels plats à un seul niveau ...
queen3
En effet, après avoir utilisé le framework pendant une durée indéterminée et recherché un code de vue propre, je pense que le ViewModel à plusieurs niveaux est inévitable. Le modèle de vue de panier dans le modèle de vue de commande par exemple.
Daniel Elliott
0

Que diriez-vous juste avant d'appeler RenderPartial?

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

Ensuite, dans votre partiel, vous avez

<%= Html.TextBox(ViewData["Prefix"] + "Name", Model.Name) %>
Anthony Johnston
la source
1
C'est fondamentalement la même chose que de le passer manuellement (il est de type sûr de dériver tous les modèles de vue de IViewModel avec "IViewModel SetPrefix (string)" à la place), et c'est très moche, à moins que je ne réécrive tous les helpers Html et RenderPartial afin qu'ils gèrent automatiquement ce. Le problème n'est pas de savoir comment le faire, je l'ai déjà résolu; le problème est que cela peut être fait automatiquement.
queen3
comme il n'y a pas d'endroit commun où vous pouvez mettre du code qui affectera tous les helpers html, vous ne pourriez pas le faire automatiquement. voir autre réponse ...
Anthony Johnston