Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC Model Binding IList in an Editor Template

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>
like image 756
A Bunch Avatar asked Oct 17 '10 00:10

A Bunch


1 Answers

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!

like image 72
tinesoft Avatar answered Sep 29 '22 22:09

tinesoft