0

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; }
}
BVernon
  • 3,205
  • 5
  • 28
  • 64
  • Yes, you can do what you want (for example using the `Prefix` property of `BindAttribute`), but without seeing some sample code, its hard to give an answer. –  Jan 18 '18 at 03:03
  • @StephenMuecke See edit. – BVernon Jan 18 '18 at 03:12
  • @StephenMuecke, did you mean `ViewData.TemplateInfo.HtmlFieldPrefix`? – Bob Dust Jan 18 '18 at 03:13
  • And what is the model in the main view (just the class definition and the `Registrants` property are necessary) And what is the controller method that you post this back to? –  Jan 18 '18 at 03:13
  • That depends on the model in the controller method. `HtmlFieldPrefix` is one option, but the better option is to use an `EditorTemplate` (which in you case will be less code as well) - assuming that the model in the POST method is the model in the main view. –  Jan 18 '18 at 03:15
  • @StephenMuecke see edit the 2nd. – BVernon Jan 18 '18 at 03:19
  • And I assume the POST method is `public ActionResult XXX(CourseRegistrationModel model)`? –  Jan 18 '18 at 03:21
  • Oh gees, I forgot the way this serialization works I'm gonna have to make the _registrants variable public. – BVernon Jan 18 '18 at 03:21
  • Oh yeah, sorry forgot to add that but yes that is what POST method looks like. – BVernon Jan 18 '18 at 03:22
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/163372/discussion-between-stephen-muecke-and-bvernon). –  Jan 18 '18 at 03:24

1 Answers1

1

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.

  • I assume the first line of the Template example was supposed to say RegistrantModel (singular). – BVernon Jan 18 '18 at 05:00
  • 1
    Yep - I did mention that your class names were confusing in our chat room :) –  Jan 18 '18 at 05:02
  • And I agreed :) – BVernon Jan 18 '18 at 05:05
  • Phew... took me a while to get this working but to do it the way I wanted I had to do 3 things. 1st two things were using option 1 and 2 of your answer because I was actually displaying the registrant entries from the registrant partial (lot of surrounding html so didn't want to move it all into main view). I'm about to try making editor for RegistrantListModel (formerly RegistrantsModel) though and see if I can nest Templates. 3rd thing was changing RegistrantListModel to inherit from IList. – BVernon Jan 18 '18 at 07:31
  • And nested Templates totally work! Can't believe I haven't used these before. I'm totally ashamed of myself for not knowing about this, lol. – BVernon Jan 18 '18 at 07:42
  • Did you also see the additional comments I made in chat about possibly changing the `RegistrantsListModel` (i.e. not inheriting `IList`, but containing a `List` instead)? –  Jan 18 '18 at 07:43
  • Yes I did and I even tried it for a minute but didn't quite get it working before I decided to try it as IList. That wasn't what the problem was, obviously, but doing it this way makes more sense to me anyway though because otherwise I'm writing "registrants.Registrants". I mean yeah I could name my variable differently so I'm writing "registrantListModel.Registrants" but it still just seems awkard. – BVernon Jan 18 '18 at 08:13
  • Unfortunately I might have celebrated prematurely on the nested Templates. Thought I had it working then realized I made a change in a wrong place and it was still doing it the old way (with combo of your 2 answers). Then fixed it right and didn't work. Can you say whether nested Templates should work? – BVernon Jan 18 '18 at 08:15
  • 1
    Yes, nested templates work (about to sign off for a hour or so, but will add some notes/code in the chat session later) –  Jan 18 '18 at 08:17
  • Ugh... I think I just realized a big problem with my testing is that I would hit the back button and use the same open tab to post each time I made changes to project cause it was easy not to have to retype all the required fields. I reloaded the page each time, but I think it doesn't quite update the same as when you get a fresh page. – BVernon Jan 18 '18 at 08:57
  • Ok, final solution I'm sticking with: RenderPartial("_Registrants", Model.RegistrantListModel) from main view and then in that partial I do EditorFor(m => m.Registrants). m.Registrants is the list exposed as you mentioned I should do. But I also have to have IList on the RegistrationItemList or it doesn't work. Kinda weird... I sorta think I understand why it works this way but not completely. Couldn't get it to work when I did nested templates though. – BVernon Jan 18 '18 at 09:29