ASP.Net MVC3 Model Binding IEnumerable<T> with Editor Template

10,064

Since you are only changing one at a time, I think the following is easier than trying to figure out at the controller which values changed, or adding a changed property and setting it via javascript.

Change Approve.cshtml to

@model IEnumerable<MvcWebsite.Models.Approve> 
<table> 
    <tr> 
        <th colspan=2> 
            Name 
        </th> 
    </tr> 
@foreach(var user in Model){
    @using (Html.BeginForm("Approve", "Registration", FormMethod.Post))         { 
    <tr> 
        <td> 
            @Html.EditorFor(m => user) 
        </td> 
        <td> 
            <input type="submit" value="Approve" class="submit-button" />             
        </td> 
    </tr> 
    }
}
</table> 

Change the Approve Editor Template to

@Html.HiddenFor(m => m.Name) 
@Html.EditorFor(m => m.Role) 
Share:
10,064
firetoast
Author by

firetoast

Updated on June 04, 2022

Comments

  • firetoast
    firetoast almost 2 years

    All, please clear up my confusion on how the model binding works with IEnumerables and Editor Templates.

    I have a view, Approve.cshtml

    @model IEnumerable<MvcWebsite.Models.Approve>
    <table>
        <tr>
            <th>
                Name
            </th>
        </tr>
        @Html.EditorForModel()
    </table>
    

    A model, Approve.cs

    public class Approve
    {
      public string Name { get;set;}
      public string Role { get; set; }
    }
    

    And an editor template

    @model MvcWebsite.Models.Approve
    
    @using (Html.BeginForm("Approve", "Registration", FormMethod.Post))
    {
    <tr>
        <td>
            @Html.HiddenFor(m => m.Name)
            @Html.EditorFor(m => m.Role)
        </td>
        <td>
            <input type="submit" value="Approve" class="submit-button" />            
        </td>
    </tr>
    

    }

    This is all fine and good. It renders the following output.

            <input name="[0].Name" type="hidden" value="" />
            ....
    

    However, in my Controller, I can't seem to receive values back for the Model (binding).

    [HttpPost]
    public ActionResult Approve(Approve approveModel)
    {
        .... approveModel has all default values
    }
    

    Can someone shed light on what I am doing wrong here? I abbreviated the code, I am using the Editor Template with other EditorFor and HiddenFor fields from my model...

    Edited: I basically have a table layout, each with the User's Name, a textbox where I can enter their role (User or Admin) and then an Approve button which submits to my controller. Hence the reason I want to only return a single Approve object. I can return the entire IEnumerable to my Controller, but if I do that, how can I tell which of the items was the one I clicked the Approve button (submit) for?

    EDIT: So I have modified the code so that I have a single form surrounding my entire View Approve.cshtml

    @model IEnumerable<MvcWebsite.Models.Approve>
    @using (Html.BeginForm("Approve", "Program", FormMethod.Post))
    {
    <table>
        <tr>
            <th>
                Name
            </th>
        </tr>
        @Html.EditorForModel()
    </table>
    }
    

    And then changed the controller to

    [HttpPost]
    public ActionResult Approve(IEnumerable<Approve> approvals)
    {
        // ???????????????????????
    }
    

    Now I'm still not clear on how to know which row I clicked Approve for. I know there are other ways to accomplish this task (create a checkbox for approve, and approve anything checked, etc.) However, I need the ability to click a button and only save 1 row back to the database, regardless if the user entered information into the other rows. Is it better practice to wrap my IEnumerable inside of it's own model (i.e. AllApprovals) and then add helper properties to that parent model (SelectedIndex, etc.)? If that is the approach to take, then how do I set the SelectedIndex after clicking an Approve button? Is that still jquery magic or is there a correct MVC way to accomplish this? Jquery magic seems very hackish to me?

    EDIT: Based on Brian's response, here is my final. Still doesn't feel quite right, but it works!

    View

    @model IEnumerable<MvcWebsite.Models.Approve>
    <table>
        <tr>
            <th>
                Name
            </th>
        </tr>
        @Html.EditorForModel()
    </table>
    

    Editor Template

    @using (Html.BeginForm("Approve", "Registration", FormMethod.Post))
    {
    <tr>
        <td>
            @Html.HiddenFor(m => m.Name)
            @Html.EditorFor(m => m.Role)
        </td>
        <td>
            <input type="submit" value="Approve" class="submit-button" />            
        </td>
    </tr>
    }
    

    Controller

    [HttpPost]
    public ActionResult Approve([Bind(Prefix="approval")]Approve approval)    {
        // WORKS!
    }
    
  • firetoast
    firetoast over 12 years
    To follow up, if I bind to the IEnumerable<Approve>, how can I tell which of the forms posted? I am essentially wrapping each Approve (EditorTemplate) with it's own form. Within that form I have an EditorFor field, where I am entering a value (User or Admin) and then submitting each individual row as it's own Approve model back to the controller. I'm guessing there is a better way to do what I am trying to do..
  • Martin Booth
    Martin Booth over 12 years
    You don't need a new form per row, just create a propert on the approve model called id and use @Html.Hidden to render it. This way you can work out a mapping between approve model modified and the new value
  • firetoast
    firetoast over 12 years
    I'm marking this as the correct answer, because it worked. However, is this the "right" way to do this? I read somewhere else on SO that if you are using a Foreach in the View, you are doing something wrong. Also, I needed to use a binding prefix in my controller for this to work [Bind(Prefix = "approval")]
  • Brian Cauthon
    Brian Cauthon over 12 years
    It sounds like it is right for you, for now. You make come back and look at the code a year from now with more experience and knowledge about the system you are building and think "wtf was I thinking". It is good to worry about whether it is "right" but it is also good to get stuff done. As in all things, moderation is the key.
  • smdrager
    smdrager over 11 years
    Stumbled across this question, but the above is NOT correct as it produces invalid markup--putting a form element anywhere in a table besides in a <td> (or th, etc.) is invalid. It may "work", but it is hacky.