126

Suppose I have ViewModel like

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

In the view I can render a partial with

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

In the partial I'll do

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

However, the problem is that both will render name="Name" while I need to have name="Child.Name" in order for model binder to work properly. Or, name="Child2.Name" when I render the second property using the same partial view.

How do I make my partial view automatically recognize the required prefix? I can pass it as a parameter but this is too inconvenient. This is even worse when I want for example to render it recursively. Is there a way to render partial views with a prefix, or, even better, with automatic reconition of the calling lambda expression so that

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

will automatically add correct "Child." prefix to the generated name/id strings?

I can accept any solution, including 3-rd party view engines and libraries - I actually use Spark View Engine (I "solve" the problem using its macros) and MvcContrib, but did not find a solution there. XForms, InputBuilder, MVC v2 - any tool/insight that provide this functionality will be great.

Currently I think about coding this myself but it seems like a waste of time, I can't believe this trivial stuff is not implemented already.

A lot of manual solutions may exists, and all of them are welcome. For example, I can force my partials to be based off IPartialViewModel<T> { public string Prefix; T Model; }. But I'd rather prefer some existing/approved solution.

UPDATE: there's a similar question with no answer here.

Community
  • 1
  • 1
queen3
  • 15,333
  • 8
  • 64
  • 119

11 Answers11

113

You can extend Html helper class by this :

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

    }

and simply use it in your views like this :

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

and you will see everything is ok!

TryingToImprove
  • 7,047
  • 4
  • 30
  • 39
Mahmoud Moravej
  • 8,705
  • 6
  • 46
  • 65
  • 18
    This will be incorrect for nested partial rendering. You need to append the new prefix to the old prefix from `helper.ViewData.TemplateInfo.HtmlFieldPrefix` in the form of `{oldprefix}.{newprefix}` – Ivan Zlatev Mar 01 '12 at 16:29
  • @Mahmoud Your code works great, but I was finding that the ViewData/ViewBag was empty when it came time to execute code in the Partial. I found that the helper, of type HtmlHelper had a new property ViewData, which hid the base model's. With that, I replaced `new ViewDataDictionary(helper.ViewData)` with `new ViewDataDictionary(((HtmlHelper)helper).ViewData)`. Do you see any problem with that? – Pat Newell Aug 20 '12 at 17:20
  • @IvanZlatev Thanks. I will correct the post after testing it. – Mahmoud Moravej Aug 22 '12 at 07:25
  • @PatNewell The case you mentioned is a little strange! I didn't have any problem with the above code till now. Maybe you did some customization which caused this? Anyway I should recheck the code to see your case. – Mahmoud Moravej Aug 22 '12 at 07:27
  • 2
    @IvanZlatev is correct. Before you set the name, you should do ```string oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name;``` – kennethc Apr 27 '14 at 08:14
  • 2
    An easy fix for nested templates is to use HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name) – Aman Mahajan Dec 01 '15 at 21:20
  • Can the above code be `pull-request`ed to MVC official codebase https://github.com/aspnet/Mvc? – usr-local-ΕΨΗΕΛΩΝ Feb 01 '17 at 16:23
  • Wrong. Correct place to PR is https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/Html/PartialExtensions.cs – usr-local-ΕΨΗΕΛΩΝ Feb 01 '17 at 18:39
101

so far, i was searching for the same thing I have found this recent post:

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
  • 4,188
  • 2
  • 31
  • 30
  • 4
    Thanks for the link, this is by far the best option listed here – B Z Mar 28 '11 at 18:45
  • Genius! This had been troubling me for some time. – Paul Suart Apr 12 '11 at 22:27
  • Really nice !!! If your want to use this in a controller, you can do : ViewData.TemplateInfo.HtmlFiedPrefix = "Child1"; – billy Jun 09 '11 at 19:23
  • 34
    Super late to this party, but if you do this approach, you should use the `ViewDataDictionary` constructor that takes the current `ViewData`, or you'll lose model state errors, validation data, etc. – bhamlin Apr 06 '12 at 08:10
  • 10
    building on bhamlin's comment. you can also pass along nested prefix such as (sry this is vb.net ex): Html.RenderPartial("AnotherViewModelControl", Model.PropX, New ViewDataDictionary(ViewData) With {.TemplateInfo = New TemplateInfo() With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}}) – hubson bropa May 21 '13 at 18:03
  • 2
    Great answer, +1. Maybe it would be worth editing it to take @bhamlin comment into account – ken2k Jul 10 '14 at 09:12
  • This is a great answer; I'm just glad that my Google-Fu was strong enough to find it :D – Tobias J Mar 20 '15 at 22:07
  • 2
    Note that if you need to return this partial from a controller you'll need to set this prefix there too. http://stackoverflow.com/questions/6617768/asp-net-mvc3-add-a-htmlfieldprefix-when-calling-controller-partialview – The Muffin Man Apr 17 '15 at 16:03
12

My answer, based on the answer of Mahmoud Moravej including the comment of 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: The Mohamoud's answer is incorrect for nested partial rendering. You need to append the new prefix to the old prefix, only if it is necessary. This was not clear in the latest answers (:

asoifer1879
  • 121
  • 1
  • 5
10

Using MVC2 you can achieve this.

Here is the strongly typed view:

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

Here is the strongly typed view for the child class (which must be stored in a subfolder of the view directory called 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 />

Here is the controller:

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

Here are the custom classes:

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

And the output source:

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

Now this is complete. Set a breakpoint in the Create Post controller action to verify. Don't use this with lists however because it wont work. See my question on using EditorTemplates with IEnumerable for more on that.

Nick Larsen
  • 18,631
  • 6
  • 67
  • 96
  • Yes, it looks like it. The problems are that lists do not work (while nested view models are usually in lists) and that it's v2... which I'm not quite ready to use in production. But still good to know it will be something that I need... when it comes (so +1). – queen3 Sep 28 '09 at 20:05
  • I also came across this the other day, http://www.matthidinger.com/archive/2009/08/15/creating-a-html.displayformany-helper-for-mvc-2.aspx, you might want to see if you can figure out how to extend it for editors (though still in MVC2). I spent a few minutes on it, but kept running into problems because I am not up to par with expressions yet. Maybe you can do better than I did. – Nick Larsen Sep 28 '09 at 20:11
  • 1
    A useful link, thanks. Not that I think it's possible to make EditorFor work here, because it won't generate [0] indexes I suppose (I'd bet it doesn't support it at all yet). One solution would be to render EditorFor() output to string and manually tweak the output (append required prefixes). A dirty hack, though. Hm! I may do extension method for Html.Helper().UsePrefix() which will just replace name="x" with name="prefix.x"... in MVC v1. Still a bit of work but not that much. And Html.WithPrefix("prefix").RenderPartial() that works in pair. – queen3 Sep 28 '09 at 20:38
  • +1 for recommending the `Html.EditorFor` method to render the child form. This is precisely what editor templates are supposed to be used for. This should be the answer. – Greg Burghardt Sep 21 '16 at 15:18
10

This is an old question, but for anyone arriving here looking for a solution, consider using EditorFor, as suggested in a comment in https://stackoverflow.com/a/29809907/456456. To move from a partial view to an editor template, follow these steps.

  1. Verify that your partial view is bound to ComplexType.

  2. Move your partial view to a subfolder EditorTemplates of the current view folder, or to the folder Shared. Now, it is an editor template.

  3. Change @Html.Partial("_PartialViewName", Model.ComplexType) to @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName"). The editor template is optional if it's the only template for the complex type.

Html Input elements will automatically be named ComplexType.Fieldname.

Community
  • 1
  • 1
R. Schreurs
  • 8,587
  • 5
  • 43
  • 62
9

PartailFor for asp.net Core 2 in case someone needs it.

    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);
    }
Lukas S.
  • 5,698
  • 5
  • 35
  • 50
Rahma Samaroon
  • 682
  • 8
  • 9
5

As stated here: https://stackoverflow.com/a/58943378/3901618 - for ASP.NET Core - you can use the partial tag helper.

<partial name="AnotherViewModelControl" for="Child" />
<partial name="AnotherViewModelControl" for="Child2" />

It generates all required name prefixes.

romanoza
  • 4,775
  • 3
  • 27
  • 44
3

I came across this issue also and after much pain i found it was easier to redesign my interfaces such that i didn't need to post back nested model objects. This forced me to change my interface workflows: sure i now require the user to do in two steps what i dreamed of doing on one, but the usability and code maintainability of the new approach is of greater value to me now.

Hope this helps some.

Matt Kocaj
  • 11,278
  • 6
  • 51
  • 79
2

You could add a helper for the RenderPartial which takes the prefix and pops it in the ViewData.

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

Then a further helper which concatenates the ViewData value

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

and so in the view ...

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

in the partial ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>
Ian Nelson
  • 57,123
  • 20
  • 76
  • 103
Anthony Johnston
  • 9,405
  • 4
  • 46
  • 57
  • Yes, that's what I described in a comment to http://stackoverflow.com/questions/1488890/asp-net-mvc-partial-views-input-name-prefixes/1489017#1489017. An even better solution would be RenderPartial which takes lambda, too, which is easy to implement. Still, thanks for the code, I guess that's the most painless and timeless approach. – queen3 Sep 29 '09 at 09:33
  • 1
    why not use helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix? I'm using that in my helper and it seems to be working fine. – ajbeaven Feb 28 '11 at 22:18
1

Like you, I add Prefix property (a string) to my ViewModels which I append before my model bound input names. (YAGNI preventing the below)

A more elegant solution might be a base view model that has this property and some HtmlHelpers that check if the view model derives from this base and if so append the prefix to the input name.

Hope that helps,

Dan

Daniel Elliott
  • 22,647
  • 10
  • 64
  • 82
  • 1
    Hmm, too bad. I can imagine many elegant solutions, with custom HtmlHelpers checking properties, attributes, custom render, etc... But for example if I use MvcContrib FluentHtml, do I rewrite all of them to support my hacks? It's strange that nobody talks about it, as if everybody just uses flat single-level ViewModels... – queen3 Sep 28 '09 at 19:45
  • Indeed, after using the framework for any length of time and striving for nice clean view code, I think the tiered ViewModel is inevitable. The Basket ViewModel within the Order ViewModel for example. – Daniel Elliott Sep 28 '09 at 19:51
1

How about just before you call RenderPartial you do

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

Then in your partial you have

<%= Html.TextBox(ViewData["Prefix"] + "Name", Model.Name) %>
Anthony Johnston
  • 9,405
  • 4
  • 46
  • 57
  • 1
    This is basically the same as passing it manually (it's type safe to derive all view models from IViewModel with "IViewModel SetPrefix(string)" instead), and is very ugly, unless I rewrite all the Html helpers and RenderPartial so that they automatically manage this. The problem is not how to do it, I solved this already; the problem is, can it be done automatically. – queen3 Sep 28 '09 at 19:54
  • as there is no common place you can put code that will affect all html helpers you would not be able to do this automatically. see other answer... – Anthony Johnston Sep 29 '09 at 07:49