Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Steve Sanderson's BeginCollectionItem helper won't bind correctly

I am using Steve Sanderson's BeginCollectionItem helper and ran into a problem. I have a form that has an option to add unlimited reward fields. I am using his helper since it solved this problem with how to keep generating the fields and not have to worry about how to bind it when the form gets submitted.

I have in this same form some checkboxes that there is an unknown amount. The difference with this one versus the rewards is the unknown amount will become known after a database call and will be known by the time the code gets to the view.

So my code looks like this

  public class FrmVm
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public bool Active { get; set; }

        public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; } // this is the checkbox ones.
         public IList<RewardVms> RewardVms { get; set; } // this is the dyanmic one that I needed the helper for

        public CbCreditCardFrmVm()
        {
            Active = true;
            WarrantyFeaturesVm = new List<WarrantyFeaturesVm>();
             RewardVms = new List<RewardVms>();
        }
    }


    // view

    @foreach (var tier in Model.RewardVms)
    {
            @Html.Partial("GenerateReward", tier)   // in this partial view in the  BeginCollectionItem                 
     }



 @foreach (var warranties in Model.WarrantyFeaturesVm)
{
    using (Html.BeginCollectionItem("WarrantyFeaturesVm"))
    { 
      <span>@warranties.Name:</span>
      @Html.TextBoxFor(x => warranties.FeatureId)
      @Html.CheckBoxFor(x => warranties.HasFeature)
    }
}

I am using jquery to submit the data by using serializeArray(). When it goes to the server it bind all the dynamic ones correctly and even binds the Warranty to the Collection(the collection count is 1). Yet it never binds anything insides the WarrantyFeaturesVm, everything is left as default.

if I remove using (Html.BeginCollectionItem("WarrantyFeaturesVm")) then it won't even bind the collection.

Anyone know why it is not binding anything in the collection?

Edit

// for loop (works)
<form method="post" id="" action="" class="ui-formwizard ui-helper-reset ui-widget ui-widget-content ui-corner-all" novalidate="novalidate">

<span id="" class="step ui-formwizard-content ui-helper-reset ui-corner-all" style="display: none;">

<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" name="WarrantyFeaturesVm[0].FeatureId" id="WarrantyFeaturesVm_0__FeatureId" data-val-required="The FeatureId field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default">    <span>Purchase</span>
<input type="checkbox" value="true" name="WarrantyFeaturesVm[0].HasFeature" id="WarrantyFeaturesVm_0__HasFeature" data-val-required="The HasFeature field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default"><input type="hidden" value="false" name="WarrantyFeaturesVm[0].HasFeature" class="ui-wizard-content ui-helper-reset ui-state-default">

</form>




//foreach loop beginItemCollection(does not work)


<form method="post" id="" action="" class="ui-formwizard ui-helper-reset ui-widget ui-widget-content ui-corner-all" novalidate="novalidate">

<span id="" class="step ui-formwizard-content ui-helper-reset ui-corner-all" style="display: inline;">

<input type="hidden" value="68ba9241-c409-4f4b-96da-cce13b127c1e" autocomplete="off" name="WarrantyFeaturesVm.index" class="ui-wizard-content ui-helper-reset ui-state-default">
<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" name="WarrantyFeaturesVm[68ba9241-c409-4f4b-96da-cce13b127c1e].war.FeatureId" id="WarrantyFeaturesVm_68ba9241-c409-4f4b-96da-cce13b127c1e__war_FeatureId" data-val-required="The FeatureId field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default">            <span>Purchase</span>
<input type="checkbox" value="true" name="WarrantyFeaturesVm[68ba9241-c409-4f4b-96da-cce13b127c1e].war.HasFeature" id="WarrantyFeaturesVm_68ba9241-c409-4f4b-96da-cce13b127c1e__war_HasFeature" data-val-required="The HasFeature field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default"><input type="hidden" value="false" name="WarrantyFeaturesVm[68ba9241-c409-4f4b-96da-cce13b127c1e].war.HasFeature" class="ui-wizard-content ui-helper-reset ui-state-default">

</span>

</form>





//for loop beginItemCollection (does not work)
<form method="post" id="" action="" class="ui-formwizard ui-helper-reset ui-widget ui-widget-content ui-corner-all" novalidate="novalidate">


<span id="" class="step ui-formwizard-content ui-helper-reset ui-corner-all" style="display: none;">

<input type="hidden" value="fe3fbc82-a2df-476d-a15a-dacd841df97e" autocomplete="off" name="WarrantyFeaturesVm.index" class="ui-wizard-content ui-helper-reset ui-state-default">
<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" name="WarrantyFeaturesVm[fe3fbc82-a2df-476d-a15a-dacd841df97e].WarrantyFeaturesVm[0].FeatureId" id="WarrantyFeaturesVm_fe3fbc82-a2df-476d-a15a-dacd841df97e__WarrantyFeaturesVm_0__FeatureId" data-val-required="The FeatureId field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default">            <span>Purchase</span>
<input type="checkbox" value="true" name="WarrantyFeaturesVm[fe3fbc82-a2df-476d-a15a-dacd841df97e].WarrantyFeaturesVm[0].HasFeature" id="WarrantyFeaturesVm_fe3fbc82-a2df-476d-a15a-dacd841df97e__WarrantyFeaturesVm_0__HasFeature" data-val-required="The HasFeature field is required." data-val="true" class="ui-wizard-content ui-helper-reset ui-state-default"><input type="hidden" value="false" name="WarrantyFeaturesVm[fe3fbc82-a2df-476d-a15a-dacd841df97e].WarrantyFeaturesVm[0].HasFeature" class="ui-wizard-content ui-helper-reset ui-state-default">

</span>

<span id="adminSettings" class="step ui-formwizard-content ui-helper-reset ui-corner-all" style="display: inline;">

</form>
like image 275
chobo2 Avatar asked Dec 19 '11 22:12

chobo2


1 Answers

Ok I think I see what is going on here.

In the second sample, where you did the foreach, it looks like your cshtml was something like this (@ symbols may be incorrect):

foreach (var war in Model.WarrantyFeaturesVm) {
    using (Html.BeginCollectionItem("WarrantyFeaturesVm")) {
        Html.HiddenFor(m => war.FeatureId)
        <span>@Html.DisplayFor(m => war.Name)</span>
        Html.HiddenFor(m => war.HasFeature)
    }
}

Because BeginCollectionItem uses its context to derive the HTML names and id's, this is why you end up with "war" in the id's and names. The model binder is looking for a collection property named "WarrantyFeaturesVm", which it finds. However it is then looking for a property named "war" on the WarrantyFeaturesVm viewmodel, which it cannot find, and thus does not bind.

<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" 
    name="WarrantyFeaturesVm[68ba9241-c409-4f4b-96da-cce13b127c1e].war.FeatureId" 
    id="WarrantyFeaturesVm_68ba9241-c409-4f4b-96da-cce13b127c1e__war_FeatureId" .../>

In the 3rd scenario, it is similar. It is looking for the WarranyFeaturesVm collection property, which it finds. It however looks for another collection item.

<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" 
    name="WarrantyFeaturesVm[fe3fbc82-a2df-476d-a15a-dacd841df97e].WarrantyFeaturesVm[0].FeatureId" 
    id="WarrantyFeaturesVm_fe3fbc82-a2df-476d-a15a-dacd841df97e__WarrantyFeaturesVm_0__FeatureId" .../>

In order to bind correctly, your HTML has to look similar to your first HTML example:

<input type="hidden" value="68ba9241-c409-4f4b-96da-cce13b127c1e" 
    name="WarrantyFeaturesVm.index" .../>
<input type="hidden" value="6aa20677-d367-4e2a-84f0-9fbe00deb191" 
    name="WarrantyFeaturesVm[68ba9241-c409-4f4b-96da-cce13b127c1e].FeatureId" 
    id="WarrantyFeaturesVm_68ba9241-c409-4f4b-96da-cce13b127c1e__FeatureId" .../>

Like I hinted in my comment, you can achieve this by putting the BeginCollectionItem and everything it wraps into a partial view. The partial view will then receive its own context, since your helpers will use the view's @Model property with the stongly-typed helpers like so: @Html.WidgetFor(m => m.PropertyName).

On the other hand, if you really need the collection to be rendered in the outer view, I don't see any problem using normal indexing (integer-based) with a for loop and without BeginCollectionItem.

Update

I dug up this old post from Phil Haack. An excerpt:

...by introducing an extra hidden input, you can allow for arbitrary indices. In the example below, we provide a hidden input with the .Index suffix for each item we need to bind to the list. The name of each of these hidden inputs are the same, so as described earlier, this will give the model binder a nice collection of indices to look for when binding to the list.

<form method="post" action="/Home/Create">

    <input type="hidden" name="products.Index" value="cold" />
    <input type="text" name="products[cold].Name" value="Beer" />
    <input type="text" name="products[cold].Price" value="7.32" />

    <input type="hidden" name="products.Index" value="123" />
    <input type="text" name="products[123].Name" value="Chips" />
    <input type="text" name="products[123].Price" value="2.23" />

    <input type="hidden" name="products.Index" value="caliente" />
    <input type="text" name="products[caliente].Name" value="Salsa" />
    <input type="text" name="products[caliente].Price" value="1.23" />

    <input type="submit" />
</form>

BeginCollectionItem uses this indexing method to make sure the model binding happens. The only difference is it uses Guids instead of ints as the indexer. But you could manually set any indexer like in Phil's example above.

like image 141
danludwig Avatar answered Jan 03 '23 13:01

danludwig