Possible bug in ASP.NET MVC with form values being replaced - asp.net-mvc

I appear to be having a problem with ASP.NET MVC in that, if I have more than one form on a page which uses the same name in each one, but as different types (radio/hidden/etc), then, when the first form posts (I choose the 'Date' radio button for instance), if the form is re-rendered (say as part of the results page), I seem to have the issue that the hidden value of the SearchType on the other forms is changed to the last radio button value (in this case, SearchType.Name).
Below is an example form for reduction purposes.
<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
<%= Html.RadioButton("SearchType", SearchType.Date, true) %>
<%= Html.RadioButton("SearchType", SearchType.Name) %>
<input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>
<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
<%= Html.Hidden("SearchType", SearchType.Colour) %>
<input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>
<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
<%= Html.Hidden("SearchType", SearchType.Reference) %>
<input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>
Resulting page source (this would be part of the results page)
<form action="/Search/Search" method="post">
<input type="radio" name="SearchType" value="Date" />
<input type="radio" name="SearchType" value="Name" />
<input type="submit" name="submitForm" value="Submit" />
</form>
<form action="/Search/Search" method="post">
<input type="hidden" name="SearchType" value="Name" /> <!-- Should be Colour -->
<input type="submit" name="submitForm" value="Submit" />
</form>
<form action="/Search/Search" method="post">
<input type="hidden" name="SearchType" value="Name" /> <!-- Should be Reference -->
<input type="submit" name="submitForm" value="Submit" />
</form>
Please can anyone else with RC1 confirm this?
Maybe it's because I'm using an enum. I don't know. I should add that I can circumvent this issue by using 'manual' input () tags for the hidden fields, but if I use MVC tags (<%= Html.Hidden(...) %>), .NET MVC replaces them every time.
Many thanks.
Update:
I've seen this bug again today. It seems that this crops its head when you return a posted page and use MVC set hidden form tags with the Html helper. I've contacted Phil Haack about this, because I don't know where else to turn, and I don't believe that this should be expected behaviour as specified by David.

Yes, this behavior is currently by design. Even though you're explicitly setting values, if you post back to the same URL, we look in model state and use the value there. In general, this allows us to display the value you submitted on postback, rather than the original value.
There are two possible solutions:
Solution 1
Use unique names for each of the fields. Note that by default we use the name you specify as the id of the HTML element. It's invalid HTML to have multiple elements have the same id. So using unique names is good practice.
Solution 2
Do not use the Hidden helper. It seems like you really don't need it. Instead, you could do this:
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
Of course, as I think about this more, changing the value based on a postback makes sense for Textboxes, but makes less sense for hidden inputs. We can't change this for v1.0, but I'll consider it for v2. But we need to think through carefully the implications of such a change.

Same as others I would have expected the ModelState to be used to fill the Model and as we explicitly use the Model in expressions in the view, it should use the Model and not ModelState.
It's a design choice and I do get why: if validations fail, the input value might not be parseable to the datatype in the model and you still want to render whatever wrong value the user typed, so it's easy to correct it.
The only thing I don't understand is: why isn't it by design that the Model is used, which is set explicitly by the developer and if a validation error occurred, the ModelState is used.
I have seen many people using workarounds like
ModelState.Clear(): Clears all ModelState values, but basically disables usage of default validation in MVC
ModelState.Remove("SomeKey"): Same as ModelState.Clear() but needs micromanagement of ModelState keys, which is too much work and it doesn't feel right with the auto binding feature from MVC. Feels like 20 years back when we were also managing Form and QueryString keys.
Rendering HTMLthemselves: too much work, detail and throws away the HTML Helper methods with the additional features.
An example: Replace #Html.HiddenFor by <input type="hidden" name="#NameFor(m => m.Name)" id="#Html.IdFor(m=>m.Name)" value="#Html.AttributeEncode(Model.Name)">. Or replace #Html.DropDownListFor by ...
Create custom HTML Helpers to replace default MVC HTML Helpers to avoid the by-design issue. This is a more generic approach then rendering your HTML, but still requires more HTML+MVC knowledge or decompiling System.Web.MVC to still keep all other features but disable ModelState precedence over Model.
Apply the POST-REDIRECT-GET Pattern: this is easy in some environments, but harder in the ones with more interaction/complexity. This pattern has it's pros and cons and you shouldn't be forced to apply this pattern because of a by-design choice of ModelState over Model.
Issue
So the issue is that the Model is filled from ModelState and in the view, we set explicitly to use the Model. Everybody expects the Model value (in case it changed) to be used unless there's a validation error; then the ModelState can be used.
Currently, in the MVC Helper extensions, the ModelState value gets precedence over the Model value.
Solution
So the actual fix for this issue should be: for each expression to pull the Model value the ModelState value should be removed if there is no validation error for that value. If there's a validation error for that input control the ModelState value shouldn't be removed and it will be used like normal.
I think this solves the issue exactly, which is better than most workarounds.
The code is here:
/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model if no validation errors exist.
/// Call this when changing Model values on the server after a postback,
/// to prevent ModelState entries from taking precedence.
/// </summary>
public static void RemoveStateFor<TModel, TProperty>(this HtmlHelper helper,
Expression<Func<TModel, TProperty>> expression)
{
//First get the expected name value. This is equivalent to helper.NameFor(expression)
string name = ExpressionHelper.GetExpressionText(expression);
string fullHtmlFieldName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
//Now check whether modelstate errors exist for this input control
ModelState modelState;
if (!helper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState) ||
modelState.Errors.Count == 0)
{
//Only remove ModelState value if no modelstate error exists,
//so the ModelState will not be used over the Model
helper.ViewData.ModelState.Remove(name);
}
}
And then we create our own HTML Helper extensions todo this before calling the MVC extensions:
public static MvcHtmlString TextBoxForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
string format = "",
Dictionary<string, object> htmlAttributes = null)
{
RemoveStateFor(htmlHelper, expression);
return htmlHelper.TextBoxFor(expression, format, htmlAttributes);
}
public static IHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
RemoveStateFor(htmlHelper, expression);
return htmlHelper.HiddenFor(expression);
}
This solution removes the issue but doesn't require you to decompile, analyze, and rebuild whatever MVC is offering you normally (don't forget also managing changes over-time, browser differences, etc.).
I think the logic of "Model value unless validation error then ModelState" should have been by-design. If it was, it wouldn't have bitten so many people, but still covered what MVC was intended todo.

I just ran into same issue. Html helpers like TextBox() precedence for passed values appear to behave exactly opposite what I inferred from the Documentation where it says:
The value of the text input element. If this value is null reference
(Nothing in Visual Basic), the value of the element is retrieved from
the ViewDataDictionary object. If no value exists there, the value is
retrieved from the ModelStateDictionary object.
To me, I read that the value, if passed is used. But reading TextBox() source:
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
seems to indicate that the actual order is the exact opposite of what is documented. Actual order seems to be:
ModelState
ViewData
Value (passed into TextBox() by caller)

Heads-up - this bug still exists in MVC 3. I'm using the Razor markup syntax (like that really matters), but I encountered the same bug with a foreach loop that produced the same value for an object property every single time.

This would be the expected behavoir - MVC doesn't use a viewstate or other behind your back tricks to pass extra information in the form, so it has no idea which form you submitted (the form name is not part of the data submitted, only a list of name/value pairs).
When MVC renders the form back, it is simply checking to see if a submitted value with the same name exists - again, it has no way of knowing which form a named value came from, or even what type of control it was (whether you use a radio, text or hidden, it's all just name=value when its submitted through HTTP).

foreach (var s in ModelState.Keys.ToList())
if (s.StartsWith("detalleProductos"))
ModelState.Remove(s);
ModelState.Remove("TimeStamp");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage1");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage2");
return View(model);

This issue still exists in MVC 5, and clearly it's not considered a bug which is fine.
We're finding that, although by design, this is not the expected behavior for us. Rather we always want the value of the hidden field to operate similarly to other types of fields and not be treated special, or pull its value from some obscure collection (which reminds us of ViewState!).
A few findings (correct value for us is the model value, incorrect is the ModelState value):
Html.DisplayFor() displays the correct value (it pulls from Model)
Html.ValueFor does not (it pulls from ModelState)
ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model pulls the correct value
Our solution is to simply implement our own Extension:
/// <summary>
/// Custom HiddenFor that addresses the issues noted here:
/// http://stackoverflow.com/questions/594600/possible-bug-in-asp-net-mvc-with-form-values-being-replaced
/// We will only ever want values pulled from the model passed to the page instead of
/// pulling from modelstate.
/// Note, do not use 'ValueFor' in this method for these reasons.
/// </summary>
public static IHtmlString HiddenTheWayWeWantItFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
object value = null,
bool withValidation = false)
{
if (value == null)
{
value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
}
return new HtmlString(String.Format("<input type='hidden' id='{0}' name='{1}' value='{2}' />",
htmlHelper.IdFor(expression),
htmlHelper.NameFor(expression),
value));
}

Example to reproduce the "design problem", and a possible workaroud.
There is no workaround for the 3 hours lost trying to find the "bug" though ...
Note that this "design" is still in ASP.NET MVC 2.0 RTM.
[HttpPost]
public ActionResult ProductEditSave(ProductModel product)
{
//Change product name from what was submitted by the form
product.Name += " (user set)";
//MVC Helpers are using, to find the value to render, these dictionnaries in this order:
//1) ModelState 2) ViewData 3) Value
//This means MVC won't render values modified by this code, but the original values posted to this controller.
//Here we simply don't want to render ModelState values.
ModelState.Clear(); //Possible workaround which works. You loose binding errors information though... => Instead you could replace HtmlHelpers by HTML input for the specific inputs you are modifying in this method.
return View("ProductEditForm", product);
}
If your form originally contains this: <%= Html.HiddenFor( m => m.ProductId ) %>
If the original value of "Name" (when the form was rendered) is "dummy", after the form is submitted you expect to see "dummy (user set)" rendered.
Without ModelState.Clear() you'll still see "dummy" !!!!!!
Correct workaround:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
I feel this is not a good design at all, as every mvc form developer needs to keep that in mind.

This may be 'by design' but it's not what is documented:
Public Shared Function Hidden(
ByVal htmlHelper As System.Web.Mvc.HtmlHelper,
ByVal name As String, ByVal value As Object)
As String
Member of System.Web.Mvc.Html.InputExtensions
Summary: Returns a hidden input tag.
Parameters:
htmlHelper: The HTML helper.
name: The form field name and System.Web.Mvc.ViewDataDictionary key used to look up the value.
value: The value of the hidden input. If null, looks at the System.Web.Mvc.ViewDataDictionary and then System.Web.Mvc.ModelStateDictionary for the value.
This would seem to suggest that ONLY when the value parameter is null (or not specified) would the HtmlHelper look elsewhere for a value.
In my app, I've got a form where: html.Hidden("remote", True) is rendering as
<input id="remote" name="remote" type="hidden" value="False" />
Note the value is getting over-ridden by what is in the ViewData.ModelState dictionary.
Or am I missing something?

So in MVC 4 the "design problem" still there. Here's the code I had to use in order to set the correct hidden values in a collection since regardless of what I do in the controller, the view always showed incorrect values.
OLD code
for (int i = 0; i < Model.MyCollection.Count; i++)
{
#Html.HiddenFor(m => Model.MyCollection[i].Name) //It doesn't work. Ignores what I changed in the controller
}
UPDATED code
for (int i = 0; i < Model.MyCollection.Count; i++)
{
<input type="hidden" name="MyCollection[#(i)].Name" value="#Html.AttributeEncode(Model.MyCollection[i].Name)" /> // Takes the recent value changed in the controller!
}
Did they fixed this in MVC 5?

There is workaround:
public static class HtmlExtensions
{
private static readonly String hiddenFomat = #"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">";
public static MvcHtmlString HiddenEx<T>(this HtmlHelper htmlHelper, string name, T[] values)
{
var builder = new StringBuilder(values.Length * 100);
for (Int32 i = 0; i < values.Length;
builder.AppendFormat(hiddenFomat,
htmlHelper.Id(name),
values[i++].ToString(),
htmlHelper.Name(name)));
return MvcHtmlString.Create(builder.ToString());
}
}

As others have suggested, I went with using direct html code instead of using the HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor etc.).
The problem though with this approach is that you need to put the name and id attributes as strings. I wanted to keep my model properties strongly-typed so I used the NameFor and IdFor HtmlHelpers.
<input type="hidden" name="#Html.NameFor(m => m.Name)" id="#Html.IdFor(m=>m.Name)" value="#Html.AttributeEncode(Model.Name)">
Update:
Here's a handy HtmlHelper extension
public static MvcHtmlString MyHiddenFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, object htmlAttributes = null)
{
return new MvcHtmlString(
string.Format(
#"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">",
helper.IdFor(expression),
helper.NameFor(expression),
GetValueFor(helper, expression)
));
}
/// <summary>
/// Retrieves value from expression
/// </summary>
private static string GetValueFor<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
{
object obj = expression.Compile().Invoke(helper.ViewData.Model);
string val = string.Empty;
if (obj != null)
val = obj.ToString();
return val;
}
You can then use it like
#Html.MyHiddenFor(m => m.Name)

Related

HTML.CheckBox Behaviour

I am using a model where a check box is there . where i am posting the form i always get more than one boolean value, Code is as follow
//controller code
// GET: /Home/
// GET: /Home/Test
public ActionResult HomeTest()
{
HomeTest ht = new HomeTest();
return View(ht);
}
[AcceptVerbs(HttpVerbs.Post)]
[ValidateInput(false)]
public ActionResult HomeTest(FormCollection collection, HomeTest model)
{
string str = collection["test"].ToString();
return View(model);
}
//View Html
<%= Html.CheckBox("test") %>
I am getting following value in str while debugging "true,false". Am i do anything wrong?
This is how Html.CheckBox was designed!
It always renders
<input name="test" type="checkbox" value="true" />
followed by a
<input name="test" type="hidden" value="false" />.
When you submit the form with the checkbox checked, you'll get test=true,false and when the checkbox is unchecked, you get test=false.
This is how it supports binding to a boolean value, because the ,false part is ignored. However, if you're binding to something else (such as string), that's when you'll see the "true,false" value.
If you want different behavior, you'll have to roll-your-own Html.CheckBoxAlt method. Here's a similar question, along with some code, that I wrote a while back: Passing checkbox selection through to an action
MVC does things this way so it can tell the difference between "There is no checkbox called 'test'" and "The checkbox called 'test' is unchecked." Since HTML doesn't provide any built-in way to tell the difference, MVC makes it always send a "false" value, which will get overridden by a "true" value if you check the box.
The easiest solution is to make better use of MVC's approach. Rather than using the FormCollection, just use a parameter that the model binder can bind to:
public ActionResult HomeTest(bool test, HomeTest model)

input type="hidden" vs htmlhelper strange issue

I spent an hour trying to figure out why the value in my hidden field was a different one than the one I was expecting. As a last ditch effort I switched it to a hidden field and it started rendering as the one I was expecting. Why would that happen??
Some context is that the ID that the one using the htmlhelper is using is the same one in the querystring ID parameter.
// renders 123
#using (Html.BeginForm()){
<input type="hidden" name="id" value="#Model.ID" />
}
vs
// renders 456
#using (Html.BeginForm()){
#Html.Hidden("id", Model.ID)
}
I believe this is part of the naming convention of MVC. This has happened to me as one of my Properties of a Model was "Title", and it conflicted with the ViewBag.Title. It started displaying the ViewBag.Title instead of the actual Model's Title.
I believe this only happens when you explicitly state "Model.ID" in the HtmlHelper. It looks at the property name "ID", then looks through the ViewContext and finds query string "ID" and uses it.
It doesn't use the query string "ID" for the one that doesn't use the HtmlHelper because it doesn't look through the ViewContext to find that name; it simply puts whatever the value is from the Model. HtmlHelpers generally look at the ViewContext and figure out what value to use just based on the Property Name rather than where it exactly came from.
If you want to use an HtmlHelper, try this and see what it does:
#Html.HiddenFor(model => model.ID)

hidden form element problem - asp.net mvc

I create a strongly typed form like this in my controller:
return View("BlaForm", Bla);
In the view I use something like this:
(1)
<%= Model.Version %>
(2)
<%= Html.Hidden("Version", Model.Version)%>
Here (1) is just for debugging purposes.
After a successive update of my object this produces something like this:
(1)
10
(2)
<input id="Version" name="Version" type="hidden" value="9" />
The hidden value is out of synch for some strange reason ???!!! The Version value was definitely 10 in this case as established by the debugger. Why is this? Are hidden value somehow cached?
Thanks.
Christian
PS:
I also do:
if (TempData["ViewData"] != null)
{
ViewData = TempData["ViewData"] as ViewDataDictionary;
}
in the controller action to maintain form values in case validation errors happen. This seems to be the reason. But still I explicitely do: <%= Html.Hidden("Version", Model.Version)%> .... ???? Maybe I missunderstand the lif cycle a bit?
Html helper will always use the value in the GET or POST request before the value in your model or ViewData. This means that if you post Version=9 to a controller action and inside this action you try to modify its value to 10, when you return the View, the Html.Hidden helper will use the POSTed value and not the one in your Model. The only workaround is a custom HTML helper or simply:
<input id="Version" name="Version" type="hidden" value="<%= Model.Version %>" />
HTML helper will always look for values in ModelStateDictionary, then in ViewData and after this use the value parameter given into helper method.
The 2 other places are in your case.
ModelState state = this.ViewData.ModelState["Version"];
state.Value; // this is the value out of the ModelStateDictionary
object value = this.ViewData["Version"]; // this is the value if set
// out of the ViewData Collection
The ModelStateDictionary gets its entries, while model binding. If you have the Version as a action method parameter, the Modelbinder (in your case the DefaultModelBinder) will enter the key version with the supplied value of get or post request.
If you change the value, put it in your model, you have to update the ModelStateDictionary too.

LabelFor and TextBoxFor don't generate the same id's

When using the following code, the id's of the field and the id in the for attribute of the label isn't identical.
<%: Html.LabelFor(x => x.Localizations["en"]) %> => Localizations[en]
<%: Html.TextBoxFor(x=> x.Localizations["en"]) %> => Localizations_en_
<%: Html.LabelFor(x => x.Localizations["en"].Property) %>
=> Localizations[en]_Property
<%: Html.TextBoxFor(x=> x.Localizations["en"].Property) %>
=> Localizations_en__Property
I traced the code in reflector and saw that the way the values are generated are different. Not using the same helper method.
LabelFor uses HtmlHelper.GenerateIdFromName and TextBoxFor uses TagBuilder#GenerateId.
Does anyone know the reason for this, or a workaround (except writing your own entire set of input/textarea/select helpers)? Or is it a bug?
UPDATE:
Since I anyway use a html helper for the label with a second parameter for the label text, I did modify it to use the same id generation code as the form field helpers.
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string labelText)
{
// The main part of this code is taken from the internal code for Html.LabelFor<TModel, TValue>(...).
var metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
var fieldName = ExpressionHelper.GetExpressionText(expression);
TagBuilder builder = new TagBuilder("label");
// Generate the id as for the form fields (adds it to self).
builder.GenerateId(fieldName);
// Use the generated id for the 'for' attribute.
builder.Attributes.Add("for", builder.Attributes["id"]);
// Remove the id again.
builder.Attributes.Remove("id");
builder.SetInnerText(labelText);
return MvcHtmlString.Create(builder.ToString());
}
This solves my immediate problem, but it doesn't answer the question as to why the implementation looks like it does in MVC2. If there's an reason for it.
By the way: There's no need to actually modify the id/for attribute in HTML5, since it's perfectly legal to have an id looking like ^~[] if you'd like to. All major browsers support it. This is nicely explained by Mathias Bynens.
UPDATE 2:
This does not solve the problem at all actually, since the DefaultModelBinder can't bind to it anyway. Using nested objects in dictionaries doesn't seem to be supported by the field name generator in MVC 2, since it generates:
<input type="text" name="Dict[en]" value="(VALUE)">
Instead of what the model binder wants:
<input type="hidden" name="Dict[0].Key" value="en">
<input type="text" name="Dict[0].Value" value="(VALUE)">
Strange that it comes out of the box this way.
I've tried create a custom model binder for it, but I can't get MVC2 to use it whatever I try to use it on:
ModelBinders.Binders.Add(typeof(IDictionary<string,object>), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(IDictionary<string,string>), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(IDictionary), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(Dictionary), new DictionaryModelBinder());
So right now it looks like it's back to manually creating the name attribute values with hidden .Key fields.
This is a bug in MVC3 that we are planning on fixing for the next release (MVC 3 RTM). LabelFor will go through the tagbuilder to generate the 'for' attribute using the same logic that is used to generate ids so they will line up for arrays and nested types.
We are currently using the html 4.01 spec to generate ids so you cannot use ids that begin with non-letters. We will think about what the best approach should be now that the standards have changed.
MVC intentionally mungs ids with special characters which are significant in jQuery/CSS3 selectors. This is because the selector syntax becomes complicated when there are "reserved" (by jQuery/CSS3) characters in the ID.
It does not do this with name because it isn't necessary there and gets in the way of binding.
It's certainly a bug if LabelFor doesn't actually point to the corresponding TextBoxFor. But I'd argue the bug is in LabelFor, not in TextBoxFor.

How do you handle the output of a dynamically generated form in ASP.NET MVC?

Say you create a form using ASP.NET MVC that has a dynamic number of form elements.
For instance, you need a checkbox for each product, and the number of products changes day by day.
How would you handle that form data being posted back to the controller? You can't set up parameters on the action method because you don't know how many form values are going to be coming back.
Just give each checkbox a unique name value:
<input class="approveCheck" id="<%= "approveCheck" + recordId %>"
name="<%= "approveCheck" + recordId %>" type="checkbox" />
Then parse the list of form values in the Action, after submit:
foreach (var key in Request.Form.Keys)
{
string keyString = key.ToString();
if (keyString.StartsWith("approveCheck", StringComparison.OrdinalIgnoreCase))
{
string recNum = keyString.Substring(12, keyString.Length - 12);
string approvedKey = Request.Form["approveCheck" + recNum];
bool approved = !String.IsNullOrEmpty(approvedKey);
// ...
You don't need to pass form values as arguments; you can just get them from Request.Form.
One other option: write a model binder to change the list into a custom type for form submission.
Per Craig's answer.. that is safer. There are quirks to posting multiple form elements with the same name. I would add that it would be wise to wrap the logic that makes the "collection" of controls in a way similar to WebForms. Web Forms prepend the container control's name and adds an index. For example, in a Repeater the form elements inside would be named (something like) RepeaterName_Element1, RepeaterName_Element2. When you go to get the elements out, you have to use FindControl or something of the sort.
Depending on the binders you are using, this should work:
<%var i = 0;
foreach (var product (IList<ProductSelection>)ViewData["products"]) {%>
<%=Html.Hidden(string.Format("products[{0}].Id", i), product.Id)%>
<%=Html.Checkbox(string.Format("products[{0}].Selected", i))%>
<%=product.Name%><br/>
<%}%>
...which will result in HTML something like this (notice the array notation on the names):
<input name="products[0].Id" type="hidden" value="123">
<input name="products[0].Selected" type="checkbox">
Widget
<input name="products[1].Id" type="hidden" value="987">
<input name="products[1].Selected" type="checkbox">
Gadget
...and the controller method that handles the post:
public ActionResult SelectProducts(IList<ProductSelection> products)
{
...
}
Upon binding, products parameter will contain two instances of ProductSelection.
One caveat is that I have not used the new default binding for complex objects. Rather I am using either the NameValueDeserializer or CastleBind, both from MvcContrib. They both behave this way. I am guessing binding in the Beta will work the same way.
Depending on your data, you could either output a 'CheckboxList' (which is not possible in the newer versions any more) and use a string[] parameter, or you could set up multiple forms and just modify the action.

Resources