ASP.NET MVC Binding Nested List - c#

I am using the solution suggested in Mel's space (https://mleeb.wordpress.com/2013/11/23/editing-nested-lists-in-asp-mvc-4/)
for editing nested list
Basically I have my model as below
ProductEditModel
--> ProductAudioEditModel
--> ProductAssetResourceEditModel
I got this working for the below
#Html.EditorFor(c => resource.TrackTitle, null, Html.GetHtmlName("TrackTitle"))
which gives me the correct value when it's edited.
However, I couldnt get this working for DropDownList or I am not able to pick the edited value in the dropdownlist . It always give me the original value in my controller.
#using (Html.BeginCollectionItem("ProductAssetAudios", out parentIdentifier))
{
.....
#foreach (var resource in Model.ProductAssetResources.OrderBy(a => a.ResourceNumber))
{
string childIdentifier = null;
#using (Html.BeginChildCollectionItem("ProductAssetResources", parentIdentifier, out childIdentifier))
{
#Html.HiddenFor(model => resource.AssetResourceStatusId, new { Name = Html.GetHtmlName(childIdentifier, "AssetResourceStatusId") })
#Html.DropDownListFor(model => resource.AssetResourceStatusId, new SelectList(visibleResourceStatuses, "AssetResourceStatusId", "Name", resource.AssetResourceStatusId), new { #class = "inherit-title" }) #Html.ValidationMessageFor(model => resource.AssetResourceStatusId)
}
}
}
The AssetResourceStatusId always holding the original value even though the drop down list is selected for a different value.
I was hoping that the EditorFor and DropDownListFor should be work in the same manner when editing nested list.
Edited
Generated HTML
DropDownListFor
<select class="inherit-title valid" id="ProductAssetAudios_0df86a5c-0a32-4b0f-97ee-3b3254f743d9__ProductAssetResources_c58ba43c-6081-41d4-88fd-d59799c7374e__resource_AssetResourceStatusId" name="ProductAssetAudios[0df86a5c-0a32-4b0f-97ee-3b3254f743d9].ProductAssetResources[c58ba43c-6081-41d4-88fd-d59799c7374e].resource.AssetResourceStatusId" aria-invalid="false"><option value="3">Extra</option>
<option selected="selected" value="2">Found</option>
<option value="8">Ignore</option>
</select>
HiddenFor
<input name="ProductAssetAudios[b5670a6a-7a1d-4c76-86bc-85a05cd144c1].ProductAssetResources[aa378d38-0fb7-4304-9f24-79d0efcb36b9].AssetResourceStatusId" data-val="true" data-val-number="The field AssetResourceStatusId must be a number." data-val-required="The AssetResourceStatusId field is required." id="ProductAssetAudios_b5670a6a-7a1d-4c76-86bc-85a05cd144c1__ProductAssetResources_aa378d38-0fb7-4304-9f24-79d0efcb36b9__resource_AssetResourceStatusId" type="hidden" value="2">
-Alan-

You model contains a collection property named ProductAssetAudios (typeof ProductAudioEditModel) and each object in that collection contains a collection property named ProductAssetResources (typeof ProductAssetResourceEditModel) and each of those objects contains a property named AssetResourceStatusId.
In C# code, if you were to get the AssetResourceStatusId value of the 1st ProductAssetResourceEditModel in the 1st ProductAudioEditModel, your code would be
var id = model.ProductAssetAudios[0].ProductAssetResources[0].AssetResourceStatusId;
Drop the model prefix and that is exactly how the name attribute of the control must be. What the BeginCollectionItem() and BeginChildCollectionItem() methods do is to modify the collection indexers to a Guid and adds a hidden input for the indexer to allow you to dynamically add and remove items from the collection. By default, the DefaultModelBinder will bind collections with zero-based consecutive indexers, unless a value for the indexers is also posted (i.e the reason why the hidden input is added).
In your case, the name attribute for the hidden input is correct, i.e. using
#Html.HiddenFor(model => resource.AssetResourceStatusId, new { Name = Html.GetHtmlName(childIdentifier, "AssetResourceStatusId") })
because your overriding the default name attribute generated by HiddenFor(). You just need to do the same for the DropDownListFor() method, i.e. set the name attribute using new { Name = Html.GetHtmlName(childIdentifier, "AssetResourceStatusId") }. But then you also need to then delete the hidden input because the DefaultModelBinder will only bind the first value that is posted for a property. Note also that you will need to change the ValidationMessageFor() also.
Side note. From the comments it appears that you are not wanting to add and remove items in the view, in which case, do not use the BeginCollectionItem() and BeginChildCollectionItem() methods. Instead, just use nested for loops or custom EditorTemplates for typeof ProductAudioEditModel and ProductAssetResourceEditModel. An example of using for loops would be
for(int i = 0; i < Model.ProductAssetAudios.Count; i++)
{
#Html.TextBoxFor(m => m.ProductAssetAudios[i].SomeProperty)
....
for (int j = 0; j < Model.ProductAssetAudios[i].ProductAssetResources.Count; j++)
{
#Html.DropDownListFor(m => m.ProductAssetAudios[i].ProductAssetResources[j].AssetResourceStatusId, new SelectList(.....)
Refer also this answer for an example of using a nested EditorTemplate.

Related

#Html.EnumDropDownListFor - Dropdown values not being set

I have a basic form allowing users to input details which then gets posted and saved to a database - this works as expected without any issues:
#model R32.Register.Models.RegisterCar
#{
ViewBag.Title = "Edit Your R32";
}
<h2>Edit R32</h2>
<div>
#using (Html.BeginForm("UpdateCar", "Garage", FormMethod.Post))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Enter details</legend>
<ol>
<li>
#Html.LabelFor(m => m.NumberPlate)
#Html.EditorFor(m => m.NumberPlate, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.NumberPlate)
</li>
<li>
#Html.LabelFor(m => m.Edition)
#Html.EnumDropDownListFor(m => m.Edition, "Select an edition:", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Edition)
</li>
<li>
#Html.LabelFor(m => m.Colour)
#Html.EnumDropDownListFor(m => m.Colour, "Select a colour:", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Colour)
</li>
</ol>
<input type="submit" value="Save Changes" />
</fieldset>
}
</div>
Model snippet:
[Required]
[Display(Name="Edition")]
public MkEnum? Edition { get; set; }
Enum:
public enum MkEnum
{
[Display(Name="Mk4")]
Mk4 = 1,
[Display(Name="Mk5")]
Mk5 = 2
}
The control renders as expected, with the Edition dropdownlist having three values: "Select an edition", "Mk4", and "Mk5".
The user is able to select an edition, control is validated, then posted to the controller.
The Post is successful, and all selected values are sent to the controller - the app then persists the data in a database, and so on, without any problems.
The issue is when I pass this model back into the same View to allow the user to edit the saved data, the saved values for the enums are NOT being set as the selected value in the dropdownlist.
I can confirm that any saved string values, such as NumberPlate in this example, are being passed back into the view and loaded into the UI.
Putting a breakpoint on the viewmodel as it renders I can confirm that my #model contains the saved values for enum properties - Edition for example - but the end result is that the "Select an edition:" dropdown list is rendered containing the expected dropdown values, but it's value is the default "Select an edition:" instead of the actual value passed in via. m.Edition.
I have been able to get this working using DropDownListFor - but am having difficulties in understanding why this is not working using EnumDropDownListFor, as this clearly seems like a more elegant solution.
Does anyone have any help/advice for this?
I just ran into this problem myself. This happens because fields of type enum are being passed back to the browser serialized as their enum names, but #Html.EnumDropDownListFor generates its option values as integers. The browser can't match up the two so the dropdown stays at its default selection.
There are 3 ways to get around this.
Get the view model's enum field to serialize properly as an int.
Write a dropdown generator that uses enum names as option values.
Use javascript to manually select the option (includes razor syntax here)
$("#YourDropdownID option").each(function () {
if ($(this).html() == '#(Html.DisplayFor(o => o.YourEnumFieldName))') {
$(this).attr("selected", "selected");
return;
}
});
Ok, so from what I could see the problem was caused by using an ActionLink to pass back the full model of an item being edited. Everything was being sent back in a Query string, so my Enum values were being passed to the controller in the following way: mkEnum=Mk4.
I was then loading the UpdateCar view as seen above in my example - but the query string values were being persisted in the call back to the View.
EnumDropDownListFor is unable to interpret/convert the text value of enums into their actual values - if I manually edited the Query string to mkEnum=1, then the correct value wasloaded into the ViewModel.
In addition to this problem, it was not a good solution passing the full model back to the controller.
I've modified the code to pass back a single Id of the item being edited - the controller then verifies the user has access to that Id, retrieves the Model from the Database then passes it back to the same View as in my above example.
With this change my dropdowns are now being updated with their values without any issues.
TLDR; If you experience this issue check to make sure you don't have model properties, specifically enum values represented by their string values, in a query string when loading your view and using EnumDropDownListFor.

Binding a complex model to a drop down list in MVC

I'm trying to bind a model that has two properties - one Int, and one Boolean - to a drop-down list in MVC. The boolean is a discriminator and the integer an ID. It is not possible to split the drop down list in two.
Here is my code so far.
<select class="col-md-3 form-control" name="Model.ID" id="model-select">
<option value="0" selected>Select an Option</option>
#foreach (var m in models.OrderBy(x => x.Id))
{
<option value="#m.ID" data-discriminator="#m.Discriminator">
#m.Name
</option>
}
</select>
The model looks something like this
class MyModel
{
int ID { get; set; }
string Name { get; set; }
boolean Discriminator { get; set; }
}
The aim is to provide a set of models to the View, then the user can pick one of these. Unfortunately each model has two properties which are used to identify which model was selected in the database - the Id, which mirrors the Id in the database, and the Discriminator. The two types are otherwise incompatible in the database, hence the discriminator. For the sake of design, I only want to have these two in the same drop-down list, as you can only select one at a time anyway.
My idea of a solution was to create 2 hidden fields which would be bound to the model like so
<input type="hidden" name="Model.ID" />
<input type="hidden" name="Model.Discriminator" />
These would be updated via JavaScript and then bound to the model (as far as I know, using names like that will bind it correctly, providing that the destination property on the model passed to the POST is Model in this example).
Are there any other alternatives I could pursue?
EDIT: Also worth noting that this 'Model' is part of a more complex model and is not the only field being POSTed, so if that makes any difference...
A select box is only going to be able to post one thing, and using JavaScript to populate hidden fields, while perhaps a workable solution, seems very brittle. Your best bet would like be creating an intermediary property that you can bind to and include both sets of information as the option value:
public string SelectedThing
{
get { return string.Format("{0},{1}", ID, Discriminator); }
set
{
if (value != null)
{
var parts = value.Split(',');
if (parts.Length == 2)
{
Int32.TryParse(parts[0], out ID);
Boolean.TryParse(parts[1], out Discriminator);
}
}
}
}
Then you would need to compose your select list in a similar way:
ViewBag.MyModelChoices = myModels.Select(m => new SelectListItem
{
Value = string.Format("{0},{1}", m.ID, m.Discriminator),
Text = m.Name
});
And finally, you would bind to this new property in your view:
#Html.DropDownListFor(m => m.SelectedThing, ViewBag.MyModelChoices)

Add prefix to control id and still have it bind MVC Razor

I have a case where I have a page displaying an order and tabs that display the order details. The order details are quite complex and since they are the same layout, I want to use a partial view or editor template that will generate the form.
The problem is the result is multiple duplicate form input id's are generated (one for each order detail. For example, I have:
foreach (var orderDetail in Model.OrderDetils)
{
#Html.EditorFor(model => orderDetail, "WorkOrder", orderDetail)
}
I've read much about this and see solutions where it is recommended to use an editortemplate, but that solution only works when you have the same form to render, but passing it different model properties so the control id's prefixes will differ...ie. like this solution.
In my case, this won't work as the model property I am passing is always the same.
So how else can I create unique Id's in the partial or editor template that will also bind.
I know instead of:
#Html.EditorFor(model => model.WOHdr.Attribute1)
I could do:
#Html.TextBoxFor(model => model.WOHdr.Attribute1, new { id = Model.Id + "_Attribute1" })
But then it won't bind when it passes to the controller.
Thoughts?
Try this
#Html.TextBoxFor(model => model.WOHdr.Attribute1, new { #id = #Model.Id + "_Attribute1" })
Use "#"+dynamic value. Now You will get unique Id's
In EditorFor you can use like this
#Html.EditorFor(model => model.WOHdr.Attribute1, null, "id=" + #Model.Id + "" )
the id will generate like this
id="id_1", id="id_2" and so on..
<input id="Checkbox1_#(test.TestId)" type="checkbox" />
i hope upper code will help you

IEnumerable property is null after submit

I am new to MVC and have some difficulties understanding this.
To make it simple, I have a "Person" object and this object has an IEnumerable property called "EmailaddressList".
I have generated an edit page through Visual Studio 2012. The main objects properties, are generated on the edit page with textboxes like Name and LastName.
However the list of e-mail addresses in the IEnumerable list of sub-objects are not generated automatically in my view.
This is OK, I have written that code by hand using a tab for each type of e-mailaddress.
So far so good.
Problem:
When I recieve the model (person object) in my HTTP-Post method, the EmailAddressList is null.
Why is it like this, It was not null when I sent it to the view.
I the tab where the e-mailadresses are listed is in a partial view.
Can anyone give me some tips, is it something I'm missing here?*
View-Code
<div id="tabs">
<ul>
#foreach (var item in Model.EmailAddressList)
{
<li>#Html.Label(item.AddressType)</li>
}
</ul>
#foreach (var item in Model.EmailAddressList)
{
<div id="#item.AddressType">
<p>
#Html.TextBoxFor(s => item.EmailAddress, new { #class = "input-xxlarge" })
</p>
</div>
}
</div>
Controller (recieving method)
Here person.EmailAddressList is null
[HttpPost]
public ActionResult Create(Person person)
{
if (ModelState.IsValid)
{
personRepository.InsertOrUpdate(person);
personRepository.Save();
return RedirectToAction("Index");
}
else
{
return View();
}
}
That's because in order to correctly index your fields (so model binder can do it's work), you have to use a for loop.
First, change your IEnumerable to be a List (so we can use an indexor in the view).
Then change your foreach to be the following for loop:
#for (int i = 0; i < Model.EmailAddressList.Count; i++)
{
<div id="#Model.EmailAddressList[i].AddressType">
<p>
#Html.TextBoxFor(m => m.EmailAddressList[i].EmailAddress, new { #class = "input-xxlarge" })
</p>
</div>
}
Based on your update, the reason this doesn't work is because the default model binder only relies on order for a collection of simple data. When it comes to complex type you need to provide the relevant index per item otherwise it doesn't know which item property your referring to e.g.
#for (int i = 0; i < Model.EmailAddressList.Count; i++) {
Html.TextBoxFor(m => m.EmailAddressList[i].EmailAddress) %>
}
See Phil Haack's article on model binding to a list.
It's due to your elements not being ID'd the correct thing for MVC to pick them up on the post back, what you need is:
#Html.EditorFor(model => model.EmailAddressList);
Then, please refer to my post located here on how to make this look to how you want it to.

Trouble with name iterations for Html.DropDownList

I have a table where each row contains fields for a form.
I iterate in the view using this:
#foreach (var item in Model) { }
Within the foreach, I create a new table row with various fields contained inside. e.g.:
<tr><td> #Html.DropDownList("granny", "") </td></tr>
"granny" is being passed via the controller, and looks a bit like this:
ViewData["granny"] = new SelectList(db.getGrannies(), "grannyid", "grannyname");
Everything's working pretty well. The view is getting its grannies, and everything looks good. I noticed however that the name property of the field { e.g....
<select id="granny" name="granny">
} is the exact same for EVERY row created. This is a problem. I want to toss this data back to the controller in the form of a FormCollection and do fun stuff with all these grannies. I can't do that if they're not all getting passed.
That is, I'd like the selects to read, instead, like this:
<select id="granny1" name="granny1">
<select id="granny2" name="granny2">
I researched the problem a bit, and tried using
new { #Name="xyz" + n }
But Visual Studio didn't like that much.
The short and sweet of it all is this:
How do I give ViewData-generated Html.DropDownLists their own unique ids/names?
How do I give ViewData-generated Html.DropDownLists their own unique ids/names?
You cannot change the name of the generated input field and this is by design. Only the id could be changed. This being said you shouldn't need to do that. You could do the following:
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DropDownList(
"selectedGranny",
(IEnumerable<SelectListItem>)ViewData["granny"]
)
</td>
</tr>
}
It seems like you are either trying to databind a collection, or you just need to manually name your selects (as they are really different controls on the web form).
You can use the following overload to pass html parameters to the drop down :
#Html.DropDownList(
"CategoryID",
(SelectList)ViewBag.CategoryId,
"--Select One--",
new{ //anonymous type
name = "granny1",
#class = "myCssClass",
onchange = "someFunction();"
})

Categories