ASP.NET MVC Model Binding IList in an Editor Template
Asked Answered
A

2

8

I am attempting to bind a list that is part of a larger view model without resorting to a custom model binder. When I use an editor template to build the list of inputs, the generated names are not in the correct format for the default binder to work.

Instead of Items[3].Id like I would expect it is Items.[3].Id. If I build the list without an editor template it works as expected.

Am I doing something obviously wrong or is this just a quirk of Html.Hidden and Html.TextBox?

public class ItemWrapper
{
  [UIHint("ItemList")]
  public IList<Item> Items { get; set; }
}

public class Item
{
  public Guid Id { get; set; }
  public string Name { get; set; }
  public int Value { get; set; }
}

Index.aspx

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

  <h2>Index</h2>

  <% using(Html.BeginForm()) 
  {%> 
    <%:Html.EditorFor(m => m.Items) %>
  <%}%>
</asp:Content>

ItemList.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IList<Mvc2Test.Models.Item>>" %>

<h4>Asset Class Allocation</h4>
<% if(Model.Count > 0) { %>
<table>
  <tbody>
    <% for(int i = 0; i < Model.Count; i++) 
    {%>
      <tr>
        <td><%: Model[i].Name%></td>
        <td>
          <%: Html.HiddenFor(m => m[i].Id) %>
          <%: Html.TextBoxFor(m => m[i].Value) %>
        </td>
      </tr>
    <%}%>
  </tbody>
</table>
<%
}%>

Output

<tr>
  <td>Item 4</td>
  <td>
    <input id="Items__3__Id" name="Items.[3].Id" type="hidden" value="f52a1f57-fca8-4bc5-a746-ee0cef4e05c2" />
    <input id="Items__3__Value" name="Items.[3].Value" type="text" value="40" />
  </td>
</tr>

Edit (Action Method)

public ActionResult Test()
{
  return View(
    new ItemWrapper
    {
      Items = new List<Item>
      {
        { new Item { Id = Guid.NewGuid(), Name = "Item 1", Value = 10 } },
        { new Item { Id = Guid.NewGuid(), Name = "Item 2", Value = 20 } },
        { new Item { Id = Guid.NewGuid(), Name = "Item 3", Value = 30 } },
        { new Item { Id = Guid.NewGuid(), Name = "Item 4", Value = 40 } }
      }
    });
}

Edit #2

HttpPost Action

[HttpPost]
public ActionResult Test(ItemWrapper w)
{
    if(w.Items == null)
        Response.Write("Items was null");
    else
        Response.Write("Items found " + w.Items.Count.ToString());
    return null;
}

Index.aspx

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h4>Does Not Work</h4>
<% using(Html.BeginForm("Test", "Home")) 
{%> 
        <%:Html.EditorFor(m => m.Items) %>
        <input type="submit" value-"Go" />
<%}%>

<h4>Does Work</h4>
        <% using(Html.BeginForm("Test", "Home")) 
        {%> 
    <table>
        <tbody>
            <% for(int i = 0; i < Model.Items.Count; i++) 
            {%>
            <tr>
                <td><%: Model.Items[i].Name%></td>
                <td>
                    <%: Html.HiddenFor(m => Model.Items[i].Id) %>
                    <%: Html.TextBoxFor(m => Model.Items[i].Value) %>
                </td>
            </tr>
            <%}%>
        </tbody>
    </table>
             <input type="submit" value-"Go" />
        <%}%>

</asp:Content>
Aceae answered 17/10, 2010 at 0:28 Comment(0)
R
7

I have understood your problem, and i might very well have a solution too :)!

First, let me explain you what i have learned by inspecting the framework's source code (it's always a good idea to inspect an opensource project's source code to better understand how certain things work).

1-) When using simple strongly typed html helpers (i.e. all Html.xxxFor(...) methods except EditorFor and DisplayFor), in the lambda expression defining the model's property to render, the name of the html element generated is equals to whatever string follows "model=>", minus what comes before "=>", that is to say:

  • the string "model" if the model is a collection
  • or the string "model." (note the "." at the end) otherwise.

So, for example this :

<%: Html.TextBoxFor( m=>m.OneProperty.OneNestedProperty)%>

will generate this html output:

<input type="text" name="OneProperty.OneNestedProperty" ../>

And this:

<%: Html.TextBoxFor( m=>m[0].OneProperty.OneNestedProperty)%>

will generate this:

<input type="text" name="[0].OneProperty.OneNestedProperty" ../>

==>This partly explains why you've got this "strange" html output when using EditorFor.

2-) When using complex strongly typed helpers (EditorFor and DisplayFor), the same previous rule is applied inside the associated partial view (ItemList.ascx in your case), and in addition, all generated html elements will be prefixed by what comes after "==>", as explained in 1-).

The prefix here is "Items.", because you have this in your typed view (Index.aspx):

<%:Html.EditorFor(m => m.Items) %>

==>This completely explains the output, and why default binder doesn't work anymore with your list of Items

The solution will be to break down your ItemWrapper parameter in the [HttpPost] method, into his properties, and then use the Bind Attribute with his Prefix parameter for each complex property, like this:

    [HttpPost]
    public string Index(string foo,[Bind(Prefix = "Items.")]IList<Item> items)
    {
        return "Hello";
    }

(supposing that ItemWrapper also has a simple property named Foo of type string)

To avoid conflict,when listing the properties in the post method, i strongly recommend you to name your parameters according to each property's name (no mather the case) like i did.

Hope this will help!

Rankle answered 1/11, 2010 at 13:42 Comment(4)
So it's really a quirk in the way MVC generates the field names. The partial view is not taking into consideration that the model is a collection when generating the field name. I guess if the Items. is created at the view level instead of the partial view level then there may not be a good way to fix it. Thanks.Aceae
kondotine: sounds like a asp.net mvc bug, has anyone yet reported it?Danforth
Ok, reported myself: aspnet.codeplex.com/workitem/7711, please vote for this bug fix!Danforth
I found that it is not required to breakdown the entire view model. It is enough to just separate these problematic collections into parameters, so the code may look like public string Index(ItemWrapper wrapper,[Bind(Prefix = "Items.")]IList<Item> items). Tested in MVC 4Murine
M
-1

A more lazy solution is just to use jQuery in order to "fix" instances of this sort. Just run the following function after the page (or partial page) loads:

function makeHiddenInputBindable() {
    $('input[type="hidden"]').each(
        function (i) {
            $(this).attr('name', function () {
                return this.name.replace(/\.\[/g, "[");
            })
        }
    );
}
Muscular answered 5/11, 2010 at 2:52 Comment(1)
jQuery shouldn't be used as a crutch to rework broken HTML. Fix it on the server side.Speakeasy

© 2022 - 2024 — McMap. All rights reserved.