Trouble with name iterations for Html.DropDownList - c#

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();"
})

Related

how can I have multiple forms each generated from a list of items?

In an MVC application, I have a list of exam questions and I want to present a small number of them to the user on the same page but where each answer can be submitted separately.
So my page looks like this ....
The view code is ....
#model List<QuestionResponseVM>
#for (int i = 0; i < Model.Count(); i++)
{
using (Html.BeginForm("CheckQuestions", "Checks", FormMethod.Post, new {questResponses = Model[i] }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model[i].QuestionID)
<tr>
<td width="35%">
#Html.Raw(Model[i].QuestionText)
#Html.HiddenFor(model => model[i].QuestionText)
</td>
<td>
#Html.TextAreaFor(model => model[i].Response, new { #name = "DisplayTextEdit", #id = "DisplayTextEdit", #rows = 1, #cols = 80 })
</td>
<td width="30%">
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</div>
</td>
</tr>
}
}
My problem is I can only get data returned to the POST method for Question 1.
Here is the Controller Code ....
public class ChecksController : Controller
{
public ActionResult CheckQuestions()
{
return View(LoadQuestions());
}
// POST: Checks
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CheckQuestions(List<QuestionResponseVM> questResponses)
{
List<QuestionResponseVM> testList = new List<QuestionResponseVM>();
if (ModelState.IsValid)
{
testList = LoadQuestions(questResponses[0].QuestionID, questResponses[0].Response);
}
return View(testList);
}
private List<QuestionResponseVM> LoadQuestions (int _QuestionID = -1, string _Response = "")
{
List<QuestionResponseVM> thisList = new List<QuestionResponseVM>();
thisList.Add(new QuestionResponseVM()
{
QuestionID = 1,
QuestionText = "Question 1",
Response = (_QuestionID == 1 ? _Response : "")
});
thisList.Add(new QuestionResponseVM()
{
QuestionID = 2,
QuestionText = "Question 2",
Response = (_QuestionID == 2 ? _Response : "")
});
thisList.Add(new QuestionResponseVM()
{
QuestionID = 3,
QuestionText = "Question 3",
Response = (_QuestionID == 3 ? _Response : "")
});
return thisList;
}
}
If the Controller POST method has a parameter of QuestionResponseVM questResponses which is what I was expecting (hoping for) then null is returned from the view no matter which "Save" button is clicked.
However, if I change the parameter to a list (i.e. List<QuestionResponseVM> questResponses) then the "Save" button for Question 1 returns a list with a single item and correct data. But, any other "Save" button (e.g. Question 2 or Question 3) returns a null list.
The behaviour for scenario 1. seems counter-intuitive to me since the "Begin Form" is set to return a single model item (instance of the model) i.e. "Model[i]".
And in scenario 2., I just don't understand why it works for the first form ("Save" button) but not for the others.
I don't believe I should need to use JScript or AJAX to do this.
But clearly, I am not "connecting some dots" here.
Can someone please explain my observed behaviour and maybe give me a push in the right direction to meet this requirement.?
I would greatly appreciate any help.
Before going through your questions, I don't get what new {questResponses = Model[i] })) is doing in your forms:
using (Html.BeginForm("CheckQuestions", "Checks", FormMethod.Post, new {questResponses = Model[i] }))
{
...
}
Model[i] is a complex object. All you got there was the name of the object:
Q1: If the Controller POST method has just a single parameter
Since you're using a for loop to generate each form and inputs within the form, the name of those inputs will be in the forms of [INDEX].NAME:
By default, the model binding will bind those inputs (QuestionId, QuestionText and Response) to a matching object. QuestionResponseViewModel indeed matches that. The problem is [INDEX]. prefix.
In order for the default model binding to work, the parameter name you declare in the POST method has to be called [INDEX], i.e., [0] for the first form, [1] for the second form and so on:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CheckQuestions(QuestionResponseVM [0])
{
...
}
But you know we can't declare anything like that in C#.
The Fix for Q1
Instead of using the regular for loop, you can use foreach to generate each form. In that way, you get rid of the need for naming a parameter that's changing for each form.
Another "GOTYOU" here is that the parameter in the controller has to match the variable you declared in the for loop for each QuestionResponseViewModel:
#foreach (var qrVM in Model)
{
using(Html.BeginForm("..."))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(x => qrVM.QuestionId)
<tr>
<td>
#Html.DisplayFor(x => qrVM.QuestionId)
#Html.HiddenFor(x => qrVM.QuestionId)
</td>
...
</tr>
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CheckQuestions(QuestionResponseVM qrVM)
{
// If you name the parameter something else, it won't bind!
...
}
If you think about it, that makes sense, because you know the form will post data with keys like qrVM.QuestionId, qrVM.QuestionText back to the server. The default model binding will search for a model that has those properties and is named qrVM.
Q2: Change parameter to a list
When the first form posts back to the server, the form data in the request body will look like:
[0].RequestionId: 1
[0].RequestionText: Question 1
[0].Response: xxx
MVC model binding is still smart enough and thinks you're posting the first item of the list you declared. Hence you will see List<QuestionResponseVM> questResponses capture the correct data for the first form.
Well what about the second and third form? If you submit the data on the second form for example, the form data in the request body will look like:
[1].RequestionId: 2
[1].RequestionText: Question 2
[1].Response: xxx
MVC model binding sees it as the 2nd item of the list, but where's the 1st item? And it got confused so it couldn't bind the data to the parameter. Hence you will see NULL from the parameter List<QuestionResponseVM> questResponses.
My 2 cents
You actually cannot put a form inside a table or between table rows like this. It's considered as invalid HTML structure. It's never a good idea to use tables as structures to display data on the page anyway. Instead, You can use Bootstrap's row and columns.
I don't know why or what made you think you shouldn't need AJAX. Your case is like the best scenario to go with an AJAX approach! For example, with AJAX, the user can save each question's response individually. The page doesn't have to be refreshed.
Your save button on Question 1 is submitting the form to the controller. You will need to either have one Save/Submit button at the end of a set of questions and utilize the FormCollection object or spend time setting up JQuery/Ajax for click events on each button and removing the form element. You could have a bit of both if the button at the bottom becomes a 'Next' and then submits to a controller to get the next set of related questions.

Dynamically Access Properties in ASP.NET Razor

I'm trying to access the properties of a class dynamically in ASP.NET Razor when generating an HTML Table. This problem is normally easily solved with reflection, but the #Html.DisplayFor method is giving me issues.
I am attempting to generate an HTML table that has 3 cells per row, with the title of the item in bold as the first line of the cell, and the value of the item in the second line of the cell. The contents of the table should not include cells which are on the 'Excluded Fields' list, and I do not want to have to statically reference each column.
<table class="blpSecurityTable">
<tr>
#{
int _rowCount = 0;
foreach (var property in item.GetType().GetProperties())
{
#if (!Model.ExcludedFields.Contains(#property.Name))
{
dynamic test = #property.GetValue(item);
<td><b>#Html.DisplayFor(m => #property.Name)</b><br />#Html.DisplayFor(m => #test)</td>
_rowCount++;
}
#if (_rowCount % numCols == 0)
{
#:</tr><tr>
}
}
}
</tr>
</table>
I've tried calling #Html.DisplayFor(m => #property.GetValue(item)) but that just creates a runtime error. I can simply call #property.GetValue(item) and the value displays, but this is not ideal because I use display templates to do things like set dates to the ShortDateString format.
I understand that DisplayFor is using reflection to determine the type of the property, and that is why I am trying to use the dynamic variable to facilitate reflection for the method. However, when I run the method, it throws errors indicating the variable is not a generic parameter, and therefore cannot share its attributes. The resulting page has mostly blank values, and some cells filled in with unexpected descriptive information.
I feel like I'm getting close, but I don't know how to proceed. The page won't look right if I don't pass the values into an HTML display method, and I cannot think of any other way to get the type of table I want to be generated. Thoughts?
The issue is solved by creating a Display Template for the Security object, which then allowed me to properly use the #Html.Display Method, because the Model for the Display Template has an entry for the property.
Here is what the page code looks like now:
Display Template
#model Interface.Models.Security
#{int numCols = 3;}
<table class="blpSecurityTable">
<tr>
#{
int _rowCount = 0;
foreach (var property in Model.GetType().GetProperties())
{
#if (!BLPDLModel.ExcludedFields.Contains(#property.Name))
{
<td><b>#Html.DisplayFor(m => #property.Name)</b><br />#Html.Display(property.Name)</td>
_rowCount++;
}
#if (_rowCount % numCols == 0)
{
#:</tr><tr>
}
}
}
</tr>
</table>
Razor Page
#foreach (var item in Model.Security)
{
<div class="blpSecurityItem">
<button type="button" class="collapsible">{button text}</button>
#{
<div class="collapsible-content">
<hr />
#Html.DisplayFor(m => item)
</div>
}
</div>
Replace #Html.DisplayFor(m => #test) with #Html.Display(property.Name)
and your model property for date should have [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}")] or anything you like

ASP.NET MVC 4.5 Map Partial View to Main View on Form submit

Hello Mighty Stackoverflowers,
I'm currently working on an ASP.NET MVC 4.5 application. I need to map the input values from my partial view to my main View Model, when I submit the create form.
In my View "Create.cshtml" I call a partial view "_SwotPart.cshtml". I pass a part of my ViewModel to the Partial View, like this:
Create.cshtml:
#model MyCoolApp.BLL.Models.MainVm
#foreach (var swot in Model.Swots)
{
<tr>
#foreach (var swotPart in swot.SwotParts)
{
#Html.Partial("~/Views/Shared/_SwotPart.cshtml", swotPart)
}
</tr>
}
My partial View looks as follows, _SwotPartial.cshtml :
<td class="form-group">
#Html.TextAreaFor(model => model.Label, htmlAttributes: new { Name = nameField, ID = nameField, #class = "form-control", placeholder = Model.SwotTypeId.GetLabel() })
</td>
Anyways, when I submit my form, the values from the partial view never arrive in the controller.
Do you have any ideas how to map this properly?
Thanks!
The problem is in the input names that will be generated the way you're currently trying to achieve this. Razor needs the context of the entire list, or at least the item's position in it, in order to generate correct input names. In other words, the easiest way to solve your issue (with a caveat) is:
#for (var i = 0; i < Model.Swots.Count(); i++)
{
...
#for (var j = 0; j < Model.Swots[i].SwotParts.Count(); j++)
{
if (Model.Swots[i].SwotParts[j].SwotTypeId == SwotType.InternalHelpful || Model.Swots[i].SwotParts[j].SwotTypeId == SwotType.InternalHarmful)
{
#Html.Partial("~/Views/Shared/_SwotPart.cshtml", Model.Swots[i].SwotParts[j])
}
}
...
Then, the partial has the correct context to work with and your inputs will be named like Swots[0].SwotParts[0].Label, which the modelbinder will be able to work with.
However, the caveat here is that you're splitting this list into two loops. That's still not going to work, as you're effectively messing with the overall context of the item(s) position within the model. To fix that, you should split your list in your model, which is better anyways, as you can remove this business logic from your view:
public class SwotVm
{
...
public List<SwotPartVm> InternalSwotParts { get; set; }
public List<SwotPartVm> ExternalSwotParts { get; set; }
}
Then, you can simply iterate over each list individually, and the values will naturally post back to the appropriate list.
Given that you're using a partial to render fields for a particular class type, though, you'd be better served by creating an editor template. If you simply move your partial code to the view: Views\Shared\EditorTemplates\SwotPartVm.cshtml, then in your main view, you can just do:
#for (var i = 0; i < Model.Swots.Count(); i++)
{
...
<tr>
<th class="swot-heading">Internal</th>
#Html.EditorFor(m => m.Swots[i].InternalSwotParts)
</tr>
<tr>
<th class="swot-heading">External</th>
#Html.EditorFor(m => m.Swots[i].ExternalSwotParts)
</tr>
}
That's obvious much cleaner, and you can take this concept even further by adding a SwotVm.cshtml editor template, allowing you replace even this little bit of code with just:
#Html.EditorFor(m => m.Swots)
Note: In your SwotVm.cshtml editor template, you would only include the code for a single SwotVm. In other words, not including the for statement.
In order for your application to parse the posted values and properly and bind it to your view model. Names of posted form data needs to be like.
swots[x1].swotParts[x2].label
Where x1 is a number ranging from 0 and up for each swot.
Where x2 is a number ranging from 0 and up for each swot part in swots.
Now when you are posting, the form data names is just label.
Instead of :
#Html.TextAreaFor(model => model.Label, htmlAttributes: new { Name = nameField, ID = nameField, #class = "form-control", placeholder = Model.SwotTypeId.GetLabel() })
try :
<textarea name="swots[x1].swotParts[x2].label" class="form-control" placeholder="#Model.SwotTypeId.GetLabel()" >#Model.Label</textarea>
Don't forget to replace the x1 and x2 with a number.
You can read more about model bindings to collections here.
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

ASP.NET MVC Binding Nested List

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.

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.

Categories