Steve Sanderson's BeginCollectionItem helper won't bind correctly - asp.net-mvc

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>

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.

Related

Change the width of a SINGLE JQuery Mobile Input field?

The other two answers on this question propose overriding
div.ui-input-text {}
However, I have multiple input fields on this page and also within my project. So those answers won't work as they would effect everything on my page or project.
How can I modify a single text input?
A class designation in the input tag doesn't work. If I encapsulate the input with a div tag it still doesn't work. I'm trying to put an icon at the end but it seems all Jquery mobile text input takes the entire screen.
<input class="address" type="text" name="address" id="basic"
placeholder="Street Address, City, State" />
<input type="button" value="FindMe" data-icon="eye" data-iconpos="notext">
CSS, No Effect!
.address {
width: 200px !important;
}
Now, I could still switch to Bootstrap if that's the better way to go on this project. It seems to have .col-xs-* classes that solve this problem.
Thanks in advance.
Instead of directly setting the class on the input,jQM provides a data-attribute for inputs called data-wrapper-class (api doc: http://api.jquerymobile.com/textinput/#option-wrapperClass). This allows you to apply a class directly to the outermost wrapping DIV that jQM adds when enhancing the textbox.
<input data-wrapper-class="address" type="text" name="address" id="basic"
placeholder="Street Address, City, State" />
Working DEMO
It is maybe bit late to answer this, but maybe for others looking for the same thing (I was :-)
You can put your address in a div:
<div class="myContainer">
<label id="lbAddress">Provide the address</label>
<input class="address" type="text" name="address" id="address" />
<input class="address" type="text" name="Street" id="Street" />
<input class="address" type="text" name="City" id="City" />
<input type="button" value="FindMe" data-icon="eye" data-iconpos="notext">
</div>
<div><input class="other" type="text" name="other" id="other"/></div>
Then select your container and just the input inside that container in the CSS:
.myContainer > .ui-input-text
{
width:200px;
}
demo: https://jsfiddle.net/petitbarzun/cfazq5k5/
Reading your comments on the answer from ezanker, if you want all the inputs to appear on one line, there needs to be a container with ui-field-contain like this (the label should be there too):
<div class="ui-field-contain">
<label id="lbAddress" style="display:none"></label>
<input class="address" type="text" name="address" id="address" />
<input class="address" type="text" name="Street" id="Street" />
<input class="address" type="text" name="City" id="City" />
<input type="button" value="FindMe" data-icon="eye" data-iconpos="notext">
</div>
<div><input class="other" type="text" name="other" id="other"/></div>
The CSS then looks like this:
.ui-field-contain > #lbAddress~[class*=ui-input-text]
{
width:219px;
}
demo http://jsfiddle.net/petitbarzun/cfazq5k5/1/

Ajax.BeginForm is submitting disabled form elements

Using MVC3 and Ajax.BeginForm I surprisingly discovered that mvc ajax forms submits elements with the attribute disabled="disabled".
I have tested both select and text inputs.
I was suprised because they should not be submited and they will not when using Html.BeginForm.
Is there some hidden option or a workaround for this?
[EDIT example]
#using (Ajax.BeginForm("Action", "Control", new AjaxOptions() { HttpMethod = "POST" }))
{
<input type="text" name="_enabled" value="_enabled" />
<input type="text" name="_disabled" value="_disabled" disabled="disabled" />
<input type="submit" value="POST" />
}
Html.BeginForm will not post the _disabled value.
you can set disabled element value in other hidden element:
<input type="text" name="Id" id="Id" value="5" />
<input type="hidden" name="Id" id="Id" value="5" />
and or :
#Html.TextBoxFor(m => m.Id)
#Html.HiddenFor(m => m.Id)

Do MVC3 non-sequential hidden input indexes need to come first?

MVC3 non-sequential index hidden inputs for model binding..
<input type="hidden" name="Index" value="whatever" />
Does it matter if they go before, after, in the middle of the other related inputs to be posted?
Does it matter at all where they end up in the posted data?
For example, can they all be lumped together and it still works?
<input type="text" name="[A].Id" value="1" />
<input type="text" name="[B].Id" value="2" />
<input type="hidden" name="Index" value="A" />
<input type="hidden" name="Index" value="B" />
No, the order of your form fields does not matter, nore where they appear on the html page.
The most important factor for MVC3 is the name of the fields must match to the name of your controller/action parameter.
If you have two fields with the same name however, only one value will be returned into your action.
As long as the hidden fields are located inside of the form it should not matter the order in which they are placed. Please see code sample below. Notice how the hidden fields are put anywhere inside of the form.
#using (Html.BeginForm())
{
#Html.ValidationSummary(false, "Please correct the following errors")
#Html.HiddenFor(m => m.CoolStuffId)
#Html.Partial("_EditCoolStuff", Model)
<fieldset class="ui-grid-a">
<div class="ui-block-a"><a data-role="button" href="#Url.Action("ActionPlan", "Store", new { id = Model.StoreID })">Cancel</a></div>
<div class="ui-block-b"><button type="submit" data-theme="a">Submit</button></div>
</fieldset>
#Html.HiddenFor(m => m.TypeId)
}

Get posted values from dynamically added controls in ASP.NET MVC

I have a form in which i'm dynamically addding controls through jQuery. I need to access the values in those controls (textboxes) when posting back the form to the server. I'm sure this is a trivial problem but i just can't get my head around it.
Any help would be greatly apreciated.
When adding a multiple controls to the page, give them all the same name attribute so you can do the following in your action:
public ActionResult MyAction(string[] items)
{
// items will contain all the values in the text boxes
return View();
}
So your HTML would like like this
<input type="text" name="items" />
<input type="text" name="items" />
<input type="text" name="items" />
<input type="text" name="items" />
<input type="text" name="items" />
<input type="text" name="items" />

How to gather arbitrary length list data in ASP.NET MVC

I need to gather a list of items associated with another item from my user in a ASP.NET MVC project. I would like to have a controller action like bellow.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(int x, int y, IEnumerable<int> zKeys)
{
//Do stuff here
}
How can I setup my form to pass data in this way? If data of this particular form can't be provided, what's the next best way to pass this type of information in ASP.NET MVC?
Scott Hanselman has an excellent article on how to do this here:
ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries
http://www.hanselman.com/blog/...BindingToArraysListsCollectionsDictionaries.aspx
<form action="/url" method="post">
<input name="x" type="text" value="1" />
<input name="y" type="text" value="1" />
<div>
<input name="zKeys" value="1" />
<input name="zKeys" value="2" />
<input name="zKeys" value="3" />
<input name="zKeys" value="4" />
<input name="zKeys" value="5" />
<input name="zKeys" value="6" />
<input name="zKeys" value="7" />
</div>

Resources