2

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
  • 195
  • 1
  • 2
  • 7

3 Answers3

2

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) 
Brian Cauthon
  • 5,524
  • 2
  • 24
  • 26
  • 1
    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")] – firetoast Oct 20 '11 at 17:46
  • 3
    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. – Brian Cauthon Oct 20 '11 at 18:10
  • 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 (or th, etc.) is invalid. It may "work", but it is hacky. – smdrager Nov 02 '12 at 19:46
0

You're binding to the single Approve class, you should bind to IEnumerable<Approve>

Martin Booth
  • 8,485
  • 31
  • 31
  • To follow up, if I bind to the IEnumerable, 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.. – firetoast Oct 20 '11 at 06:13
  • 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 – Martin Booth Oct 20 '11 at 10:27
0

Martin is correct I just want to add some more information. Your rendered HTML with the [0] is special syntax the model binder looks at and assumes you are working with a list if objects. Since your action method has a single Approve class and not a kist, you are experiencing this problem.

Adam Tuliper
  • 29,982
  • 4
  • 53
  • 71