MVC can't override EditorTemplate name when used in EditorFor for child object
Asked Answered
C

1

12

I am trying to use an EditorTemplate to display a child collection in a table in the parent’s view. The problem I have run into is that this only seems to work if the template is named exactly the same as child’s class. When I attempt to use a template with a slightly different name, and pass that name as the templateName argument to EditorFor,I get a runtime error. I was hoping I could use different child EditorTemplates for different purposes with the same child collection. Here is an abbreviated example:

Models:

public class Customer
{
  int id { get; set; }
  public string name { get; set; }

  public List<Order> Orders { get; set; }
}
public class Order
{
    public int id { get; set; }
    public DateTime orderdate { get; set; }
    public decimal amount { get; set; }

    public Customer customer { get; set; }
}

Customer controller Index() method:

public ActionResult Index()
{
  Customer customer = new Customer() {id = 1, name = "Acme Corp.", Orders = new List<Order>()};
  customer.Orders.Add(new Order() {id = 1, orderdate = DateTime.Now, amount = 100M});
  customer.Orders.Add(new Order() { id = 2, orderdate = DateTime.Now, amount = 200M });
  return View(customer);
}

Customer Index.cshtml view:

@model TemplateTest.Customer

@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Customer</title>
</head>
<body>
  <div>
      @Html.EditorFor(Model=>Model.name)

      <table>
      <thead>
          <tr>
              <th>Order ID</th>
              <th>Order Date</th>
              <th>Amount</th>
          </tr>
      </thead>
          @Html.EditorFor(Model=>Model.Orders)
      </table>

  </div>
</body>
</html>

Order.cshmtl template in Views/Shared/EditorTemplates (added “color” to verify I am using this template):

@model TemplateTest.Order

<tr>
  <td>@Html.DisplayFor(Model=>Model.id)</td>
  <td style="color:blue">@Html.EditorFor(Model=>Model.orderdate)</td>
  <td>@Html.EditorFor(Model=>Model.amount)</td>
</tr>

This works fine. But if I rename the EditorTemplate to “OrderList.cshtml” and change the child EditorFor line to

@Html.EditorFor(Model=>Model.Orders, "OrderList")

when I run it again I get this exception:

“The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[TemplateTest.Order]', but this dictionary requires a model item of type 'TemplateTest.Order'.”

Any idea why the EditorFor doesn’t use the template “OrderList” I specified in the “templateName" argument? Otherwise, what is that argument for?

Chrysoberyl answered 18/5, 2013 at 22:54 Comment(1)
Hey, one more thing I noticed. The place where you set color: blue as a debugging aid won't actually show up visually. I'm too tired to explain why, but if you change it to background-color: blue I think it will do what you want it to. ;)Pricking
P
26

TL;DR > Named templates don't work with collections, use a foreach loop to work around it - See below for extensive details about why, and an example.


You said:

Any idea why the EditorFor doesn’t use the template “OrderList” I specified in the “templateName" argument? Otherwise, what is that argument for?

EditorFor is actually using the the template OrderList that you specified -- but you've stumbled on something that very confusing. Some research turned up a lot of hints but I found the real nuts-and-bolts details in this post: Problem with MVC EditorFor named template

In short, what is happening is that the default case which works:@Html.EditorFor(Model=>Model.Orders) is actually calling an MVC default template in the interim by convention, but this is not obvious at all.

Try thinking of it this way:

In the working version you are passing in a type List<Order> with the reference to Model.Orders (MANY orders) but the template is specified with a model of Order (single, NOT MANY).

Interesting. Why does that even work? At first glance it seems like it should not work. But it does work because of what happens behind the scenes.

Paraphrased from above mentioned post:

When you use @Html.EditorFor(c => c.Orders) MVC convention chooses the default template for IEnumerable. This template is part of the MVC framework, and what it does is generate Html.EditorFor() for each item in the enumeration. That template then generates the appropriate editor template for each item in the list individually - in your case they're all instances of Order, so, the Order template is used for each item.

That's the magic, and it is handy, but because this happens by convention and is basically hidden from us, it is the source of the confusion in my opinion.

Now when you try to do the same thing but using a named template by explicitly setting your EditorFor to use a particular editor template OrderList, you end up with that editor template being passed the whole enumeration -- and this is the source of the error you posted.

In other words the failing case manages to skip over the 'magic' part of the working case and that it is why it fails. But, semantically it looks good and sound, right? There's the confusion.

Working case:

your call                                default MVC template      your template
@Html.EditorFor( Model => Model.Orders)  IEnumerable template      Order template

Failing case:

your call                                           your template
@Html.EditorFor(Model=>Model.Orders, "OrderList")   OrderList template       ERROR!!!

There's a number of ways to make the error go away, but many of them are problematic because they cause the HTML controls be rendered in a way that prevents you from being able to address the individual controls by index on POST. Uhhg. (Note: the working case does render the HTML correctly as expected)

To get the HTML controls rendered properly, it seems that you must use a regular for loop (not a foreach) and pass each of the individual Order objects to the custom template (which I've called OrderEditorTemplateDefault).

@for (int i = 0; i < Model.Orders.Count ; i++) 
{
    @Html.EditorFor(c => Model.Orders[i], "OrderEditorTemplateDefault")
} 

Part of your question indicated:

I was hoping I could use different child EditorTemplates for different purposes with the same child collection.

You could do that by introducing a condition inside the loop and choosing the alternate template there (either for the entire list or on an Order-by-Order basis, just depends on how you write the condition)

@for (int i = 0; i < Model.Orders.Count ; i++) {
    if (someCondition) {
        @Html.EditorFor(c => Model.Orders[i], "OrderEditorTemplateDefault")
    } else {
        @Html.EditorFor(c => Model.Orders[i], "OrderEditorTemplateALTERNATE")
    }
} 

Sorry so verbose. Hope that helps.

Pricking answered 19/5, 2013 at 4:40 Comment(6)
Sorry for editing your post David, I just thought it might help to make a succinct statement at the outset. I would upvote you a billion times if I could.Bostow
That means a lot coming from you. If you can manage half-a-billion upvotes that would be cool too.Pricking
Wow, what a thoughtful and clear response! Thanks a lot, that will save me a lot of additional wasted time trying to figure this one out. I read a lot of posts on the subject but did not run into the one you referenced. I have used for loops in the past, but several of the posts said it was preferable to use editors with templates instead, which does make for cleaner code and takes scripts out of the markup. I may try creating my own EditorForCollectioon helper, that shouldn't take much time <s>. Thanks again!Chrysoberyl
I'm new here. I clicked on the check mark and it turned green. Did that do it? BTW, I found a good EditorForCollection at link. I changed it to accept the child model name as the 2nd parameter and used that in the EditorFor call, and it worked like a charm.Chrysoberyl
What a brilliant and clear answer. Five other articles here didn't really help me solve the issue, but this made quick work of it.Phosphoroscope
I noticed that the blurb at the top of this answer says to “use a foreach to work around it,” but answer shows a for loop working and a foreach loop failing.Phosphoroscope

© 2022 - 2024 — McMap. All rights reserved.