Question

I have a View in which the user can choose any number of Clubs by selecting checkboxex. The Clubs are a property of the main model with type List<ClubModel>. While refactoring I start out with this:

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Voor Select clubs </legend><br />
    <table>
        <tr>
            @for (var i = 0; i < Model.Clubs.Count; i++)
            {
                if (i % 3 == 0)
                {
                    @:</tr><tr> 
                }
                <td>
                    @Html.HiddenFor(model => model.Clubs[i].ClubID)
                    @Html.EditorFor(model => model.Clubs[i].IsAvailable)
                </td> 
                <td>@Html.DisplayFor(model => model.Clubs[i].ClubName)</td>
             }
        </tr>
     </table>
        <input type="submit" value="Submit" />
    </fieldset>
}

This works fine: the model is returned with a populated Clubs property.

Now I take out the <td> tags and move them to an EditorTemplate:

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Select Clubs </legend><br />
           <table>
        <tr>
            @for (var i = 0; i < Model.Clubs.Count; i++)
            {
                if (i % 3 == 0)
                {
                    @:</tr><tr> 
                }
               @Html.EditorFor(model=>model.Clubs[i])
             }
        </tr>
     </table>
        <input type="submit" value="Submit" />
    </fieldset>
}

This still works (template not shown).

Now I want to move the loop too to an EditorTemplate:

@using (Html.BeginForm())
{
    <fieldset>
        <legend> Select Clubs</legend><br />
        <EditorFor(model=>model.Clubs,"ListOfClubs")
         <input type="submit" value="Submit" />
    </fieldset>
}

I duly create a EditorTemplate named 'ListOfClubs':

@using InvallersManagementMVC3.ViewModels;
@model List<StandInClubModel>
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <table>
        <tr>
            @for (var i = 0; i < Model.Count; i++)
            {
                if (i % 3 == 0)
                {
                    @:</tr><tr> 
                }
                <td>
                    @Html.HiddenFor(model => model[i].ClubID)
                    @Html.EditorFor(model => model[i].IsAvailable)
                </td> 
                <td>@Html.DisplayFor(model => model[i].ClubName)</td>
             }
        </tr>
     </table>
</body>
</html>

This correctly shows the clubs with checkboxes for the IsAvailable property, but now on posting the Clubs property of the model is null!

Where am I going wrong?

EDIT: I tried to implement Cymen's answer by using:

@Html.EditorFor(model=>model.Clubs,"ClubModel") 

or specifying the elementtemplate while passing in a list of these element. However I am greeted by an exception: System.InvalidOperationException was unhandled by user code Message=The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[InvallersManagementMVC3.ViewModels.ClubModel]', but this dictionary requires a model item of type 'InvallersManagementMVC3.ViewModels.ClubModel'.

Was it helpful?

Solution

You seem to be trying to group the model list passed to the view by 3. So in order to refactor your code I would recommend you start by introducing a proper view model => one that reflects the requirements of this specific view:

public class GroupedClubs
{
    public IEnumerable<StandInClubModel> Clubs { get; set; }
}

Now inside the controller action we should simply convert the domain model into a list of this view model:

public ActionResult Index()
{
    // This is our domain model. In a real world application
    // it would come from a service layer. I am hardcoding some
    // values here for simplicity
    var clubs = Enumerable.Range(1, 8).Select(x => new StandInClubModel
    {
        ClubID = x,
        ClubName = "club " + x
    });

    // Now we group the list of clubs by 3 in order to simplify
    // our view code and avoid writing some ugly loops and spaghetti code
    // In a real world application I would recommend externalizing this mapping
    // between the domain model and the view model into a separate mapping layer
    // AutoMapper is great for this job 
    var viewModel = clubs
        .Select((club, index) => new { club, index })
        .GroupBy(g => g.index / 3, i => i.club)
        .Select(x => new GroupedClubs
        {
            Clubs = x
        });

    return View(viewModel);
}

Now all that's left is to write some views:

~/Views/Home/Index.cshtml:

@model IEnumerable<GroupedClubs>

@using (Html.BeginForm())
{
    <fieldset>
        <legend> Select Clubs</legend>
        <br />

        <table>
            <tbody>
                @Html.EditorForModel()
            </tbody>
        </table>

        <input type="submit" value="Submit" />
    </fieldset>
}

~/Views/Home/EditorTemplates/GroupedClubs.cshtml:

@model GroupedClubs
<tr>
    @Html.EditorFor(x => x.Clubs)
</tr>

~/Views/Home/EditorTemplates/StandInClubModel.cshtml:

@model StandInClubModel
<td>
    @Html.HiddenFor(x => x.ClubID)
    @Html.EditorFor(x => x.IsAvailable)
</td>
<td>
    @Html.DisplayFor(x => x.ClubName)
</td>

and that's pretty much all. Now you could have a controller action which would handle the form submission:

[HttpPost]
public ActionResult Index(List<GroupedClubs> clubs)
{
    ... map the view model back to some domain model and pass
        to the service layer for processing
}

OTHER TIPS

Make a EditorFor one instance of ClubModel and let ASP.NET MVC render it (let it do the iteration). ASP.NET MVC has some specific naming/id schemes for the input tags and you're not rendering them in your iteration.

So use this -- same as yours but observe the template name:

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Select Clubs</legend><br />
        <table>
            <%: EditorFor(model=>model.Clubs, "Club") %>
        </table>
        <input type="submit" value="Submit" />
    </fieldset>
}

And the EditorFor. Note that it is for a single instance of the model even though you're passing in a list above. This is ASP.NET MVC "magic".

<tr>
    <td>
        @Html.HiddenFor(model => model.ClubID)
        @Html.EditorFor(model => model.IsAvailable)
    </td>
    <td>@Html.DisplayFor(model => model.ClubName)</td>
</tr>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top