MVC3 Non-Sequential Indices and DefaultModelBinder - asp.net-mvc-3

Is it true that the default model binder in MVC 3.0 is capable of handling non-sequential indices (for both simple and complex model types)? I've come across posts that suggest it should, however in my tests it appears that it does NOT.
Given post back values:
items[0].Id = 10
items[0].Name = "Some Item"
items[1].Id = 3
items[1].Name = "Some Item"
items[4].Id = 6
items[4].Name = "Some Item"
And a controller method:
public ActionResult(IList<MyItem> items) { ... }
The only values that are loaded are items 0 and 1; item 4 is simply ignored.
I've seen numerous solutions to generate custom indices (Model Binding to a List), however they all appear to targeting previous versions of MVC, and most are a bit 'heavy-handed' IMO.
Am I missing something?

I have this working, you have to remember to add a common indexing hidden input as explained in your referenced article:
The hidden input with name = Items.Index is the key part
<input type="hidden" name="Items.Index" value="0" />
<input type="text" name="Items[0].Name" value="someValue1" />
<input type="hidden" name="Items.Index" value="1" />
<input type="text" name="Items[1].Name" value="someValue2" />
<input type="hidden" name="Items.Index" value="3" />
<input type="text" name="Items[3].Name" value="someValue3" />
<input type="hidden" name="Items.Index" value="4" />
<input type="text" name="Items[4].Name" value="someValue4" />
hope this helps

This helper method, derived from Steve Sanderson's approach, is much simpler and can be used to anchor any item in a collection and it seems to work with MVC model binding.
public static IHtmlString AnchorIndex(this HtmlHelper html)
{
var htmlFieldPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
var m = Regex.Match(htmlFieldPrefix, #"([\w]+)\[([\w]*)\]");
if (m.Success && m.Groups.Count == 3)
return
MvcHtmlString.Create(
string.Format(
"<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
m.Groups[1].Value, m.Groups[2].Value));
return null;
}
E.g. Simply call it in an EditorTemplate, or anywhere else you would generate inputs, as follows to generate the index anchoring hidden variable if one is applicable.
#model SomeViewModel
#Html.AnchorIndex()
#Html.TextBoxFor(m => m.Name)
... etc.
I think it has a few advantages over Steve Sanderson's approach.
It works with EditorFor and other inbuilt mechanisms for processing enumerables. So if Items is an IEnumerable<T> property on a view model, the following works as expected:
<ul id="editorRows" class="list-unstyled">
#Html.EditorFor(m => m.Items)
#* Each item will correctly anchor allowing for dynamic add/deletion via Javascript *#
</ul>
It is simpler and doesn't require any more magic strings.
You can have a single EditorTemplate/DisplayTemplate for a data type and it will simply no-op if not used on an item in a list.
The only downside is that if the root model being bound is the enumerable (i.e. the parameter to the Action method itself and not simply a property somewhere deeper in the parameter object graph), the binding will fail at the first non-sequential index. Unfortunately, the .Index functionality of the DefaultModelBinder only works for non-root objects. In this scenario, your only option remains to use the approaches above.

The article you referenced is an old one (MVC2), but as far as I know, this is still the defacto way to model bind collections using the default modelbinder.
If you want non-sequential indexing, like Bassam says, you will need to specify an indexer. The indexer does not need to be numeric.
We use Steve Sanderson's BeginCollectionItem Html Helper for this. It automatically generates the indexer as a Guid. I think this is a better approach than using numeric indexers when your collection item HTML is non-sequential.

I was struggling with this this week and Bassam's answer was the key to getting me on the right track. I have a dynamic list of inventory items that can have a quantity field. I needed to know how many of which items they selected, except the list of items can vary from 1 to n.
My solution was rather simple in the end. I created a ViewModel called ItemVM with two properties. ItemID and Quantity. In the post action I accept a list of these. With Indexing on, all items get passed.. even with a null quantity. You have to validate and handle it server side, but with iteration it's trivial to handle this dynamic list.
In my View I am using something like this:
#foreach (Item item in Items)
{
<input type="hidden" name="OrderItems.Index" value="#item.ItemID" />
<input type="hidden" name="OrderItems[#item.ItemID].ItemID" value="#item.ItemID" />
<input type="number" name="OrderItems[#item.ItemID].Quantity" />
}
This gives me a List with a 0-based Index, but iteration in the controller extracts all the necessary data from a new strongly-typed model.
public ActionResult Marketing(List<ItemVM> OrderItems)
...
foreach (ItemVM itemVM in OrderItems)
{
OrderItem item = new OrderItem();
item.ItemID = Convert.ToInt16(itemVM.ItemID);
item.Quantity = Convert.ToInt16(itemVM.Quantity);
if (item.Quantity > 0)
{
order.Items.Add(item);
}
}
You will then end up with a collection of Items that have a quantity greater than 0, and the Item ID.
This technique is working in MVC 5 utilizing EF 6 in Visual Studio 2015. Maybe this will help someone searching for this solution like I was.

Or use this javascript function to fix the indexing: (Replace EntityName and FieldName obviously)
function fixIndexing() {
var tableRows = $('#tblMyEntities tbody tr');
for (x = 0; x < tableRows.length; x++) {
tableRows.eq(x).attr('data-index', x);
tableRows.eq(x).children('td:nth-child(1)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName1");
tableRows.eq(x).children('td:nth-child(2)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName2");
tableRows.eq(x).children('td:nth-child(3)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName3");
}
return true; //- Submit Form -
}

I ended up making a more generic HTML Helper:-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
namespace Wallboards.Web.Helpers
{
/// <summary>
/// Hidden Index Html Helper
/// </summary>
public static class HiddenIndexHtmlHelper
{
/// <summary>
/// Hiddens the index for.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="expression">The expression.</param>
/// <param name="index">The Index</param>
/// <returns>Returns Hidden Index For</returns>
public static MvcHtmlString HiddenIndexFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int index)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var propName = metadata.PropertyName;
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<input type=\"hidden\" name=\"{0}.Index\" autocomplete=\"off\" value=\"{1}\" />", propName, index);
return MvcHtmlString.Create(sb.ToString());
}
}
}
And then include it in each iteration of the list element in your Razor view:-
#Html.HiddenIndexFor(m => m.ExistingWallboardMessages, i)

Related

How to get checkbox input value bound to a property of a complex object list in MVC?

I have searched for this, and found several solutions, but none seem to work for me.
I'm using Razor to build an MVC view.
The model I'm passing in is defined as follows:
public class ActiveProxiesDTO
{
/// <summary> error message returned by the transaction </summary>
public string ErrorMsg { get; set; }
/// <summary> List of proxies returned by the transaction </summary>
public List<ActiveProxiesDetailDTO> ActiveProxies { get; set; }
}
/// <summary> A single proxy </summary>
public class ActiveProxiesDetailDTO
{
/// <summary> Proxie's PERSON id </summary>
public string ProxyId { get; set; }
/// <summary> Proxie's First Name </summary>
public string FirstName { get; set; }
/// <summary> Proxie's Last Name </summary>
public string LastName { get; set; }
/// <summary> Proxie's email address </summary>
public string EmailAddress { get; set; }
/// <summary> Does user want to reset this proxy's password?</summary>
public bool Reset { get; set; }
}
In the action method that invokes the view I set the "Reset" bool to false for all items in the list.
My view is defined as follows:
#model Ellucian.Colleague.Dtos.Base.M32.ActiveProxiesDTO
#using (Html.BeginForm("PostResetProxy", "Core", FormMethod.Post, new { Area = "M32" }))
{
#Html.AntiForgeryToken()
<div class="dataentry">
Proxies:
<br />
<table>
<tr>
<th>Reset?</th> <th>Name</th> <th>Email Address(to send new password)</th>
</tr>
#for (int i = 0; i < Model.ActiveProxies.Count; i++)
{
<tr>
<td>
<!-- make sure all data bind to the model being passed to the next controller action method -->
<input type="hidden" name="ActiveProxies[#i].ProxyId" value="#Model.ActiveProxies[i].ProxyId" />
<input type="hidden" name="ActiveProxies[#i].FirstName" value="#Model.ActiveProxies[i].FirstName" />
<input type="hidden" name="ActiveProxies[#i].LastName" value="#Model.ActiveProxies[i].LastName" />
<!-- checkboxes require both a hidden and non-hidden input element -->
<input type="hidden" name="ActiveProxies[#i].Reset" value="#Model.ActiveProxies[i].Reset" />
<input type="checkbox" name="ActiveProxies[#i].Reset" value="true" />
</td>
<td>
#{ var displayName = Model.ActiveProxies[i].FirstName + " " + Model.ActiveProxies[i].LastName; }
#displayName
</td>
<td>
<input type="text" name="ActiveProxies[#i].EmailAddress" value="#Model.ActiveProxies[i].EmailAddress" />
</td>
</tr>
}
</table>
<br />
<input type="submit" />
...
for now, the action method handling the POST is just:
[HttpPost]
public ActionResult PostResetProxy( ActiveProxiesDTO formData )
{
return Redirect(Url.Action("Index", "Home", new { Area = "" }));
}
I set a breakpoint at the return statement and check the contents of formData.
Everything looks correct: The entire ActiveProxies list is present, If I enter email addresses on the form they post correctly to the proper entry of the list.
The problem is that, whether I check the "reset" box or not, the value is always passed back as false.
I've tried using Html.CheckBox and Html.CheckBoxFor in various ways as described elsewhere in this forum, but with no success. I've tried using string instead of bool for the checkbox value, but that also failed. I'm out of ideas. Anyone see what I'm doing wrong? -- TIA Gus
I found a solution that works for me (it does not involve JavaScript (I'm not yet proficient in it).
Upon return to the method handling the Post request I checked the values in Request.Form[ "ActiveProxies[i].Reset"] and noticed that the value was always ",true" (strange that it has a "," prefix! A bug?) for every row of the table where I had "Reset?" checked, and the empty string wherever I had it unchecked.
So I got around the issue by adding logic at the top of my POST action that corrects the values in my formData argument. The (unfinished) Post method now reads as follows
[HttpPost]
public ActionResult PostResetProxy( ActiveProxiesDTO formData )
{
for ( int i=0 ; i < formData.ActiveProxies.Count ; i++ )
{
string x = Request.Form[ "ActiveProxies["+i+"].Reset"];
formData.ActiveProxies[i].Reset = x.Contains("true") ? true : false;
}
// for the time being, just return to the home page
return Redirect(Url.Action("Index", "Home", new { Area = "" }));
}
I've just discovered a better solution.
I had originally tried the #Html.CheckBox and #Html.CheckBoxFor helpers without success and therefore gave up on using helpers for my checkbox input.
To my surprise, the more general purpose "EditorFor" helper turned out to work where the ones purported to be specifically for a checkbox did not.
I replaced my original lines
<!-- checkboxes require both a hidden and non-hidden input element -->
<input type="hidden" name="ActiveProxies[#i].Reset" value="#Model.ActiveProxies[i].Reset" />
<input type="checkbox" name="ActiveProxies[#i].Reset" value="true" />
with the single line
#Html.EditorFor(m => Model.ActiveProxies[i].Reset)
and ... to my surprise, it worked, allowing me to take the kluge code out of my POST method.

Access global page variable in helper

#{
int i = 0;
}
#helper Text() {
<input type="text" name="Ans[#i].Text" />
}
i is not accessible in helper. How to access it?
You can simply add it as member to you page by using #functions declaration:
#functions
{
private int i;
}
You could pass it as parameter to the helper:
#helper Text(int i) {
<input type="text" name="Ans[#i].Text" />
}
and then:
#{
int i = 0;
}
#SomeHelper.Text(i)
or you could simply use editor templates which will take care of everything and get rid of those helpers. For example:
#Html.EditorFor(x => x.Ans)
You can achieve this by changing base class for your view. This scenario applies to situation where helper is declared in view.
Create a base class that inherits from WebViewPage and introduce shared field or property:
public class MyBasePage<T> : WebViewPage<T>
{
public int i;
public override void Execute()
{ }
}
Using #inherits directive change base class. And now field/property is acessible both from "page context" and helper:
#inherits NamespaceOfYourBaseClass.MyBasePage<YourModel>
#{
i = 0;
}
#helper Text() {
<input type="text" name="Ans[#i].Text" />
}
If you want to have a thing that is close to term "page property/field" but dont want to create a base class or helpers are stored within App_Code folder then you can try WebPageBase.Page property.
MSDN: Provides property-like access to page data that is shared between
pages, layout pages, and partial pages.
The code in this case would be:
#{
Page.i = 0;
}
#helper Text() {
<input type="text" name="Ans[#Page.i].Text" />
}
The drawback is that Page property is of type dynamic and thus does not support intellisense. As an alternative to Page there is another property - WebPageBase.PageData.
MSDN: Provides array-like access to page data that is shared between pages,
layout pages, and partial pages.
In this case a class-container of strings/ints keys for "page variables" could be created. And the code would be like:
// class visible to views and helpers
class MyViewFields {
public const string i = "MyViewFields.i"; // or maybe generate guid for key so there would be not doubts about its uniqueness.. but how would you debug this? :)
}
// in MyView.cshtml
#{
PageData[MyViewFields.i] = 0
}
#helper Text() {
<input type="text" name="Ans[#PageData[MyViewFields.i]].Text" />
}
This at least provides constraints for shared page data but still no control over value type.

MVC matching ModelState keys to ViewModel collection

Is it possible to match a ViewModel property to the matching ModelState.Key value when the ViewModel is a (has a) collection?
Example: To edit a collection of viewmodel items, I am using the extension found here.
That adds a GUID to the id of the fields on the page.
example:
class Pets
{
string animal;
string name;
}
For a list of Pets, the generated html source is like this:
<input name="Pets.index" autocomplete="off" value="3905b306-a9..." type="hidden">
<input value="CAT" id="Pets_3905b306-a9...__animal" name="Pets[3905b306-a9...].animal" type="hidden">
<input value="MR. PEPPERS" id="Pets_3905b306-a9...__name" name="Pets[3905b306-a9...].name" type="hidden">
<input name="Pets.index" autocomplete="off" value="23342306-b4..." type="hidden">
<input value="DOG" id="Pets_23342306-b4...__animal" name="Pets[23342306-b4...].animal" type="hidden">
<input value="BRUTICUS" id="Pets_23342306-b4...__name" name="Pets[23342306-b4...].name" type="hidden">
So when this gets bound on post, the ModelState gets loaded with all the form fields.
In ModelSTate.Keys, there is:
Pets[23342306-b4...].name
Pets[23342306-b4...].animal
Pets[3905b306-a9...].name
Pets[3905b306-a9...].animal
Everything good so far, but I am doing some business logic validation, things like, cant add new animal if one exists with the same name. In that case, I want to be able to highlight the input field that is in error.
So if my create function fails, it will return an error/key value pair like this:
{ error = "Duplicate Name", key="name" }
So I at least will now what property caused the problem.
But since my repository functions don't know about the view field ids, how can I match the key "name" to the appropriate ModelState key (in this case, either Pets[23342306-b4...].name or Pets[3905b306-a9...].name)?
If you used the built in functionality of MVC for displaying collections (Html.DisplayFor(m => m.Pets) or Html.EditorFor(m => m.Pets)) with appropriate display/editor template, MVC would render something like this:
Pets[0].name
Pets[0].animal
Pets[1].name
Pets[1].animal
This maps to IEnumerable<Pets> and you know that first item has index of 0, second item 1 etc.
So if the second item has an error, you can set error for the ModelState key "Pets[1].name" for example.
If you are using the Html.BeginCollectionItem extension method, like I was, I was able to get around this by not using the GUID. I need the dynamic add and delete, but I was always looking up known items, persons that have an ID, which I had in my editor. So instead of using the GUID, I just assign the ID (uniqueId) in the code below. I could then find the key because I knew it was Person[234232]. Of course if you are adding new items and not displaying selected items, it might not work for you.
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName, string uniqueId)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : uniqueId;
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}

MV3 Duplicate Query String Values for CheckBox (true,false for boolean)

I've created a fairly straight forward page with a check box:
#using (Html.BeginForm("MyController", "MyAction", FormMethod.Get))
{
#Html.CheckBoxFor(x => x.MyCheckBox)
<input type="submit" value="Go!" />
}
The URL is populated with the MyCheckBox value twice!? As such:
MyAction?MyCheckBox=true&MyCheckBox=false
It only duplicates the value if the check box is true. If set to false it will only appear once in the query string.
The code above is simplified as I have a couple of drop downs and a textbox on the form which work fine. I don't think there's anything unusual about the code which I've left out from this question.
Has anyone had a similar issue with query string parameters being duplicated?
This behaviour is by design of the checkbox control. The standard HTML checkbox control passes no value if it is not checked. This is unintuitive. Instead, the ASP.Net checkbox control has 2 elements, the standard control which is visible and also a hidden control with a value of 'False'.
Therefore, if the checkbox is not checked, there will be one value passed: False.
If it is checked, there will be two values, True and False. You therefore need to use the following code to check for validity in your code:
bool checkboxChecked = Request.QueryString["MyCheckBox"].Contains("True");
Accepted answer is correct however in my case in a recent development the MVC behaviour is misleading.
The MVC Html.CheckBox(...) and Html.CheckBoxFor(...) generate an extra input of 'type=hidden' with the same ID as the checkbox control, leading to the duplicate URL parameters. I got around this problem by simply including the mark up desired as follows:
#if(checkTrue){
<input type="checkbox" id="MyCheckBox" name="MyCheckbox" checked="checked">
}else{
<input type="checkbox" id="MyCheckBox" name="MyCheckbox">
}
Would be better wrapped upin a helper to use in place of the MVC code so the value check is encapsulated.
As part of my application, the controller maintains sets of query parameters using both form injection and link injection using helpers in order to preserve state (of paging/filtering controls for example) when clicked to navigate within the same controller scope. As a result of this feature, the check box element is always set back to false if the standard MVC helpers are used. It's a good thing I noticed and did not waste much time on this bug.
In my model, I had a collection of checkboxes like so:
public class PrerequisitesViewModel
{
public List<StudentPrerequisiteStatusViewModel> PrerequisiteStatuses { get; set; }
}
public class StudentPrerequisiteStatusViewModel
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
In order to get everything to bind correctly, I had to actually convert the values from the querystring and parse them manually with the following code:
// fix for how MVC binds checkboxes... it send "true,false" instead of just true, so we need to just get the true
for (int i = 0; i < model.PrerequisiteStatuses.Count(); i++)
{
model.PrerequisiteStatuses[i].IsSelected = bool.Parse((Request.QueryString[$"PrerequisiteStatuses[{i}].IsSelected"] ?? "false").Split(',')[0]);
}
Alas, it works, but I can't believe this is necessary in MVC! Hopefully, someone else knows of a better solution.
I solve this issue with use #Html.HiddenFor
<input id="checkboxId" type="checkbox" value="true" onchange="changeCheckboxValue()">
#Html.HiddenFor(m => m.MyCheckBox, new { #id = "hiddenId" } )
<script>
function changeCheckboxValue() {
document.getElementById("checkboxId").value = document.getElementById("hiddenId").checked;
}
</script>

ASP.Net MVC3 Parent Child Model Binding

I have a partial template that uses a User object as a model. The user has a collection of Accounts. On this partial template I have a loop as follows. The _Account partial template is bound to the Account class
#foreach (var item in Model.Accounts)
{
<tr>
<td colspan="6">
<div>
#Html.Partial("_Account", item)
</div>
</td>
</tr>
}
In my controller method I initially tried
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UserDetails(User user, string actionType)
But the User.Accounts collection is empty. Then I tried this. Still the Accounts collection is empty.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UserDetails(User user,
[Bind(Prefix="User.Accounts")]
FixupCollection<Account> Accounts,
string actionType)
Can I use the default Modelbinder implementation to achieve this or do I need to do anything different?
Yep, you can use the default model binder. You just need to name your fields correctly. So you need your loop to output something like this:
...
<input type="text" name="user.Accounts[0].SomeTextField" />
<input type="text" name="user.Accounts[0].SomeOtherTextField" />
...
<input type="text" name="user.Accounts[1].SomeTextField" />
<input type="text" name="user.Accounts[1].SomeOtherTextField" />
...
If you need to add/remove accounts, the hardcoded indexes get a little trickier. You could re-assign the names using javascript before postback. But it's all possible. This question gives more detail on model binding:
ASP.NET MVC: Binding a Complex Type to a Select
Use Editor Templates instead of a partial view - no need to hard code your indexes as the template will automagically index all your objects correctly, even when you add and remove Accounts. See my answer to this question:
Pass values from multiple partial views
Small write up on Editor Templates here:
codenodes.wordpress.com - MVC3 Editor Templates

Resources