Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 785
jimslate Avatar asked May 18 '13 22:05

jimslate


1 Answers

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.

like image 150
David Tansey Avatar answered Nov 07 '22 09:11

David Tansey