Search code examples
asp.net-mvcasp.net-mvc-3razormvc-editor-templates

EditorTemplate for List of complex type returns null, while EditorTemplate for its elements works fine


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'.


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
    }