asp.net-mvcpartial-viewsdefaultmodelbinder

How to bind properly with partial views w/o having to send in entire Model


I like to take sections of my view and break them out in to multiple partial views, and I like to only pass in the part of the model that partial view is interested in. Generally I like to put a model specifically for the partial as a property on the model that gets passed into the main view.

The problem though is I think this causes the html helpers not to render in a way that the model binder can properly put it back together since it doesn't realize in the partial that it is a property of another object.

I really like doing it this way cause it keeps the code soooo much more organized and harder for less experienced programmers to come and poop all over my code since everything is very structured for them already. And up till now this hasn't been a problem for me because I either didn't need to take form input from partials or else it was handled with ajax calls. This time I would like to just use the regular DefaultBinder though and am wondering if there is a way to make this work without having to send the entire model into all the partial views?

Example:

Main view has this line of code in it:

@{ Html.RenderPartial("_Registrants", Model.Registrants); }

The registrants partial looks like this:

@model Models.Order.RegistrantsModel

// stuff...

// important part:
@for(int i = 0; i < Model.Count(); i++)
{
    @Html.HiddenFor(o => o[i].Enabled)
    <ul class="frmRow@(Model[i].Enabled ? "" : " disabled")">
        <li>
            <span class="title">First Name</span>
            @Html.TextBoxFor(o => o[i].FirstName, new { @placeholder = "enter first name" })
            @Html.ValidationMessageFor(o => o[i].FirstName)
        </li>
        <li>
            <span class="title">Last Name</span>
            @Html.TextBoxFor(o => o[i].LastName, new { @placeholder = "enter last name" })
            @Html.ValidationMessageFor(o => o[i].LastName)
        </li>
        <li>
            <span class="title">Email Address</span>
            @Html.TextBoxFor(o => o.First().Email, new { @placeholder = "enter email address" })
            @Html.ValidationMessageFor(o => o[i].Email)
        </li>
    </ul>
}

Main model looks like this:

public class CourseRegistrationModel
{
    public CourseRegistrationModel() { }

    public CourseRegistrationModel(RegistrationItemModel itemModel, PaymentModel paymentModel)
    {
        Item = itemModel;
        Payor = new PayorModel();
        Registrants = new RegistrantsModel();
        Shipping = new ShippingModel();
        Payment = paymentModel;
    }

    public RegistrationItemModel Item { get; set; }
    public PayorModel Payor { get; set; }
    public RegistrantsModel Registrants { get; set; }
    public ShippingModel Shipping { get; set; }
    public PaymentModel Payment { get; set; }
}

And here are RegistrantsModel and RegistrantModel:

public class RegistrantsModel : IEnumerable<RegistrantModel>
{
    public RegistrantsModel()
    {
        _registrants = new List<RegistrantModel>();

        for(int i = 0; i < 5; i++)
            _registrants.Add(new RegistrantModel());

        _registrants.First().Enabled = true; // Show one registrant on form by default
    }

    List<RegistrantModel> _registrants { get; set; }
    public decimal PricePerPerson { get; set; }
    public int NoOfRegistrants { get; set; }

    public RegistrantModel this[int i]
    {
        get { return _registrants[i]; }
    }

    public IEnumerator<RegistrantModel> GetEnumerator() { return _registrants.GetEnumerator(); }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return _registrants.GetEnumerator(); }
}

public class RegistrantModel: IEnabled
{
    [RequiredIfEnabled]
    public string FirstName { get; set; }

    [RequiredIfEnabled]
    public string LastName { get; set; }

    [RequiredIfEnabled]
    [EmailAddress(ErrorMessage = "Please Enter a Valid Email Address")]
    public string Email { get; set; }

    public bool Enabled { get; set; }
}

Solution

  • Your partial is generating form controls with name attributes that relate to a collection of RegistrantModel e.g.

    <input name="[0].FirstName" ... />
    

    which would bind to a POST method with a parameter IList<RegistrantModel>. In order to bind to your CourseRegistrationModel, you inputs need to be

    <input name="Registrants[0].FirstName" ... />
    

    There are 2 options to add the correct prefix to the name attribute.

    One is to add the prefix by passing it as AdditionalViewData in the RenderPartial() method

    @{ Html.RenderPartial("_Registrants", Model.Registrants,
        new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "Registrants" }}); }
    

    Refer also getting the values from a nested complex object that is passed to a partial view for an extension method you can use to simplify the code in the view

    The preferred method is to use an EditorTemplate for typeof RegistrantModel. You need to name the partial the same as the class name (in your case RegistrantModel.cshtml) and locate it in the /Views/Shared/EditorTemplates folder (or in the /Views/YourControllerName/EditorTemplates if you want to use different templates for different controllers). Your template is then based on a single instance of the model

    @model RegistrantModel
    
    @Html.LabelFor(m => m.FirstName)
    @Html.TextBoxFor(m => m.FirstName, new { @placeholder = "enter first name" })
    @Html.ValidationMessageFor(m => m.FirstName)
    ....
    

    and in the main view, use

    @Html.EditorFor(m => m.Registrants)
    

    The EditorFor() method has overloads that accept both a single T and IEnumerable<T>, and in the case of a collection, the methods generates the correct html for each item in the collection.