Insert items to List in Razor with Model [duplicate] - c#

I have added a button in my view. When this button is clicked partial view is added. In my form I can add as much partial view as I can. When Submitting this form data I am unable to send all the partial view data to controller.
I have made a different model having all the attributes and I have made a list of that model to my main model. Can anyone please give me some trick so that I can send all the partial view content to my controller?
In My View
<div id="CSQGroup">
</div>
<div>
<input type="button" value="Add Field" id="addField" onclick="addFieldss()" />
</div>
function addFieldss()
{
$.ajax({
url: '#Url.Content("~/AdminProduct/GetColorSizeQty")',
type: 'GET',
success:function(result) {
var newDiv = $(document.createElement("div")).attr("id", 'CSQ' + myCounter);
newDiv.html(result);
newDiv.appendTo("#CSQGroup");
myCounter++;
},
error: function(result) {
alert("Failure");
}
});
}
In My controller
public ActionResult GetColorSizeQty()
{
var data = new AdminProductDetailModel();
data.colorList = commonCore.getallTypeofList("color");
data.sizeList = commonCore.getallTypeofList("size");
return PartialView(data);
}
[HttpPost]
public ActionResult AddDetail(AdminProductDetailModel model)
{
....
}
In my Partial View
#model IKLE.Model.ProductModel.AdminProductDetailModel
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
</div>
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategoryColorId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategoryColorId, Model.colorList, "--Select Color--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategoryColorId)
</div>
<div class="editor-field">
#Html.LabelFor(model => model.productTotalQuantity)
#Html.TextBoxFor(model => model.productTotalQuantity)
#Html.ValidationMessageFor(model => model.productTotalQuantity)
</div>

Your problem is that the partial renders html based on a single AdminProductDetailModel object, yet you are trying to post back a collection. When you dynamically add a new object you continue to add duplicate controls that look like <input name="productTotalQuantity" ..> (this is also creating invalid html because of the duplicate id attributes) where as they need to be <input name="[0].productTotalQuantity" ..>, <input name="[1].productTotalQuantity" ..> etc. in order to bind to a collection on post back.
The DefaultModelBinder required that the indexer for collection items start at zero and be consecutive, or that the form values include a Index=someValue where the indexer is someValue (for example <input name="[ABC].productTotalQuantity" ..><input name="Index" value="ABC">. This is explained in detail in Phil Haack's article Model Binding To A List. Using the Index approach is generally better because it also allows you to delete items from the list (otherwise it would be necessary to rename all existing controls so the indexer is consecutive).
Two possible approaches to your issue.
Option 1
Use the BeginItemCollection helper for your partial view. This helper will render a hidden input for the Index value based on a GUID. You need this in both the partial view and the loop where you render existing items. Your partial would look something like
#model IKLE.Model.ProductModel.AdminProductDetailModel
#using(Html.BeginCollectionItem())
{
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
</div>
....
}
Option 2
Manually create the html elements representing a new object with a 'fake' indexer, place them in a hidden container, then in the Add button event, clone the html, update the indexers and Index value and append the cloned elements to the DOM. To make sure the html is correct, create one default object in a for loop and inspect the html it generates. An example of this approach is shown in this answer
<div id="newItem" style="display:none">
<div class="editor-field">
<label for="_#__productTotalQuantity">Quantity</label>
<input type="text" id="_#__productTotalQuantity" name="[#].productTotalQuantity" value />
....
</div>
// more properties of your model
</div>
Note the use of a 'fake' indexer to prevent this one being bound on post back ('#' and '%' wont match up so they are ignored by the DefaultModelBinder)
$('#addField').click(function() {
var index = (new Date()).getTime();
var clone = $('#NewItem').clone();
// Update the indexer and Index value of the clone
clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
clone.html($(clone).html().replace(/"%"/g, '"' + index + '"'));
$('#yourContainer').append(clone.html());
}
The advantage of option 1 is that you are strongly typing the view to your model, but it means making a call to the server each time you add a new item. The advantage of option 2 is that its all done client side, but if you make any changes to you model (e.g. add a validation attribute to a property) then you also need to manually update the html, making maintenance a bit harder.
Finally, if you are using client side validation (jquery-validate-unobtrusive.js), then you need re-parse the validator each time you add new elements to the DOM as explained in this answer.
$('form').data('validator', null);
$.validator.unobtrusive.parse($('form'));
And of course you need to change you POST method to accept a collection
[HttpPost]
public ActionResult AddDetail(IEnumerable<AdminProductDetailModel> model)
{
....
}

Related

Passing an IEnumerable to a view to generate a form and then passing form data back to controller

I'm working on a project for my office. The end result I'm looking for is that a boilerplate letter is pulled from a database, the sections that need specific input are extracted, a form is generated from those sections, the form then returns user data, and the letter is rebuilt with the user data integrated into the text of the letter.
for example, the string pulled from the database would look like this
Claim #: |string^clmNum^Claim Number: | - Ref#: |string^RefNum^Reference Number: |
and would end up like the following after it was rebuilt with user data:
Claim #: 123456 - Ref#: 789012
This is what I have working so far...
The sections between the | are pulled out, split, and loaded into an IEnumerable
My foo model is:
public class Foo
{
public string InputType {get; set;}
public string InputName {get; set;}
public string InputLabel {get; set;}
}
I pass the IEnumerable to the view with a ViewModel
public class FormBuildViewModel
{
public IEnumerable<Foo> FooProperty {get; set;}
}
I then display the input items dynamically with the following Razor markup on my view.
<form>
#{ var e = Model.FooProperty.ToList();
foreach (var subItem in e)
{
<div class="FormGroup-items">
<label>#subItem.InputLabel</label>
<input name="#subItem.ObjName" type="text" />
</div>
}
}
<..// form button stuff //..>
</form>
Which creates the following HTML:
<form>
<div class="FormGroup-items">
<label>Claim Number: </label>
<input name="clmNum" type="text" />
</div>
<div class="FormGroup-items">
<label>Reference Number: </label>
<input name="RefNum" type="text" />
</div>
<..// button stuff //..>
</form>
I have everything working up to this point. I need to take the data entered on the dynamically created form and get it back to the controller in a manner that I can index to rebuild the string.
I've tried using the #html.beginform similar to this
#using (Html.BeginForm())
{
#for(int i=0; i<Model.Count; i++)
{
#Html.CheckBoxFor(m => m[i].IsActive, new { #value = Model[i].Id })
#Html.DisplayFor(m => m[i].Name)
}
<input type="submit" value="Save" />
}
but to use #Html.BeginForm you need to know the names of the items before runtime, and it doesn't seem to work with a dynamically created form like this.
The only thing I can think of is I need to load the form data into a List< T > and return that to the controller, but I can't think of a way to get C# to allow me to initialize a List< T > and load the values in the view. I know I've got to be missing something, but I'm kinda lost at this point. Any help would be appreciated.
Are you passing your viewmodel back to your page? This seems like you are setting the viewmodel with data atleast from a 5000 foot view:
[HttpGet]
public IActionResult MyCallMethod()
{
FooProperty = getmydatafromsomewhere();
return View();
}
Then your page would have a way to build appropriately
#model My.Name.Space.MyViewModel
#using (Html.BeginForm("Action", "Controller"))
{
#foreach (var item in #Model.FooProperty)
{
<div class="FormGroup-items">
<label asp-for="item.InputType" />
<input asp-for="item.InputType" class="form-control" />
</div>
//other data
}
}
I also assume you have a post setup on the controller.
[HttpPost]
public IActionResult MyCallMethod(MyViewModel viewModel)
{
//do something with the viewmodel here
//same page, somewhere else, do something else, etc.
}
You can use some tag helpers as well for your labels and inputs if you so chose:
#Html.LabelFor(m => item.InputType, new { #class="whateverIwant" })
#Html.TextBoxFor(m => item.InputType, new { #class="form-control" })

List hidden value passes wrong value to controller

I have a list In my view. For each row, I view button and I am passing Id value as hidden. But when I click any button it is passing wrong hidden value to the controller. Always it passes the first-row hidden value to the controller.
View:
#foreach (var list in Model)
{
<div>
<div > #( ((int)1) + #Model.IndexOf(list)).</div>
<div >#list.details</div>
<div class="col-md-2 row-index">
<button class="btn btn-link" type="submit" name="action:view" id="view">View</button>
<input type="hidden" name="viewId" id="viewId" value="list.WId" />
</div>
</div>
}
Controller:
[HttpPost]
[MultipleButton(Name = "action", Argument = "view")]
public ActionResult ViewDetail(string viewId)
{
return RedirectToAction("ViewDetails");
}
To get all values you need to change the input value type in your controller to array of strings.
I hope that this solution can help you
[HttpPost]
[MultipleButton(Name = "action", Argument = "view")]
public ActionResult ViewDetail(string[] viewId)
{
return RedirectToAction("ViewDetails");
}
if you want to get the exact value you need to duplicate the form within your foreach
in this case you should write somthing like this :
#foreach (var list in Model)
{
<div>
<div > #( ((int)1) + #Model.IndexOf(list)).</div>
<div >#list.details</div>
<div class="col-md-2 row-index">
<form ... > // complete your form attributes
<button class="btn btn-link" type="submit" name="action:view" id="view">View</button>
<input type="hidden" name="viewId" id="viewId" value="list.WId" />
</form>
</div>
</div>
}
Note : You should delete the global form
You should have one form for each row. then you submit that row.
Otherwise as you state it passes first value.
You are setting each value to the same element ID (which is invalid anyway) and name. When you submit your form (which would be more helpful to fully answer your question) it is finding the first element that matches that criteria and submitting it.
There are multiple ways to resolve this such as the already mentioned form per entry but the other preference would be to modify you button to a div and add a click handler to pass the specific value to a js function which would then submit to the controller. Its a preference choice regarding how tightly coupled you want your front end. But the main problem is your element naming convention.

Incorrect List Model Binding indices when using HTML Helpers

this is a tricky one to explain, so I'll try bullet pointing.
Issue:
Dynamic rows (collection) available to user on View (add/delete)
User deletes row and saves (POST)
Collection passed back to controller with non-sequential indices
Stepping through code, everything looks fine, collection items, indices etc.
Once the page is rendered, items are not displaying correctly - They are all out by 1 and therefore duplicating the top item at the new 0 location.
What I've found:
This happens ONLY when using the HTML Helpers in Razor code.
If I use the traditional <input> elements (not ideal), it works fine.
Question:
Has anyone ever run into this issue before? Or does anyone know why this is happening, or what I'm doing wrong?
Please check out my code below and thanks for checking my question!
Controller:
[HttpGet]
public ActionResult Index()
{
List<Car> cars = new List<Car>
{
new Car { ID = 1, Make = "BMW 1", Model = "325" },
new Car { ID = 2, Make = "Land Rover 2", Model = "Range Rover" },
new Car { ID = 3, Make = "Audi 3", Model = "A3" },
new Car { ID = 4, Make = "Honda 4", Model = "Civic" }
};
CarModel model = new CarModel();
model.Cars = cars;
return View(model);
}
[HttpPost]
public ActionResult Index(CarModel model)
{
// This is for debugging purposes only
List<Car> savedCars = model.Cars;
return View(model);
}
Index.cshtml:
As you can see, I have "Make" and "Actual Make" inputs. One being a HTML Helper and the other a traditional HTML Input, respectively.
#using (Html.BeginForm())
{
<div class="col-md-4">
#for (int i = 0; i < Model.Cars.Count; i++)
{
<div id="car-row-#i" class="form-group row">
<br />
<hr />
<label class="control-label">Make (#i)</label>
#Html.TextBoxFor(m => m.Cars[i].Make, new { #id = "car-make-" + i, #class = "form-control" })
<label class="control-label">Actual Make</label>
<input class="form-control" id="car-make-#i" name="Cars[#i].Make" type="text" value="#Model.Cars[i].Make" />
<div>
<input type="hidden" name="Cars.Index" value="#i" />
</div>
<br />
<button id="delete-btn-#i" type="button" class="btn btn-sm btn-danger" onclick="DeleteCarRow(#i)">Delete Entry</button>
</div>
}
<div class="form-group">
<input type="submit" class="btn btn-sm btn-success" value="Submit" />
</div>
</div>
}
Javascript Delete Function
function DeleteCarRow(id) {
$("#car-row-" + id).remove();
}
What's happening in the UI:
Step 1 (delete row)
Step 2 (Submit form)
Step 3 (results)
The reason for this behavior is that the HtmlHelper methods use the value from ModelState (if one exists) to set the value attribute rather that the actual model value. The reason for this behavior is explained in the answer to TextBoxFor displaying initial value, not the value updated from code.
In your case, when you submit, the following values are added to ModelState
Cars[1].Make: Land Rover 2
Cars[2].Make: Audi 3
Cars[3].Make: Honda 4
Note that there is no value for Cars[0].Make because you deleted the first item in the view.
When you return the view, the collection now contains
Cars[0].Make: Land Rover 2
Cars[1].Make: Audi 3
Cars[2].Make: Honda 4
So in the first iteration of the loop, the TextBoxFor() method checks ModelState for a match, does not find one, and generates value="Land Rover 2" (i.e. the model value) and your manual input also reads the model value and sets value="Land Rover 2"
In the second iteration, the TextBoxFor() does find a match for Cars[1]Make in ModelState so it sets value="Land Rover 2" and manual inputs reads the model value and sets value="Audi 3".
I'm assuming this question is just to explain the behavior (in reality, you would save the data and then redirect to the GET method to display the new list), but you can generate the correct output when you return the view by calling ModelState.Clear() which will clear all ModelState values so that the TextBoxFor() generates the value attribute based on the model value.
Side note:You view contains a lot of bad practice, including polluting your markup with behavior (use Unobtrusive JavaScript), creating label element that do not behave as labels (clicking on them will not set focus to the associated control), unnecessary use of <br/> elements (use css to style your elements with margins etc) and unnecessary use of new { #id = "car-make-" + i }. The code in your loop can be
#for (int i = 0; i < Model.Cars.Count; i++)
{
<div class="form-group row">
<hr />
#Html.LabelFor(m => m.Cars[i].Make, "Make (#i)")
#Html.TextBoxFor(m => m.Cars[i].Make, new { #class = "form-control" })
....
<input type="hidden" name="Cars.Index" value="#i" />
<button type="button" class="btn btn-sm btn-danger delete">Delete Entry</button>
</div>
}
$('.delete').click(function() {
$(this).closest('.form-group').remove();
}

Multiple forms created using for loops each with a submit button only first form data posts on submit [duplicate]

I have the following code, only the first form submits anything, the following submit null values, each model has data. If I change it to just one large form, everything submits. Why do the other individual forms post null values?
View
#model myModel[]
<ul>
#for (int i = 0; i < Model.Length; i++)
{
using (Html.BeginForm("controllerAction", "Controller", FormMethod.Post,
new { id="Form"+i }))
{
<li>
#Html.TextBoxFor(a => a[i].property1)
#Html.CheckBoxFor(a => a[i].property2)
#Html.HiddenFor(a => a[i].property3)
<input type="submit" />
</li>
}
}
</ul>
Controller
[HttpPost]
public ActionResult controllerAction(myModel[] models)
{
...do stuff...
}
The reason is that your creating form controls with indexers in your for loop, and your POST method parameter is myModel[] models.
By default, the DefaultModelBinder requires collection to be zero based and consecutive, so if you attempt to submit the second form, your posting back [1].property1: someValue etc. Because the indexer starts at 1, binding fails and the model is null.
You can solve this by adding a hidden input for an Index property used by the model binder to match up non consecutive indexers
<li>
#Html.TextBoxFor(a => a[i].property1)
#Html.CheckBoxFor(a => a[i].property2)
#Html.HiddenFor(a => a[i].property3)
<input type="hidden" name="Index" value="#i" /> // add this
<input type="submit" />
</li>

MVC model Null on post when using Partial view

I have an MVC controller where the model on the post method always comes back as null. I'm not sure if this is because I am using a partial view within the form.
Any idea why the model is not being returned to the controller?
Model
Loading the model
public List<Group> GetStaticMeasures(int businessUnitID)
{
List<Group> groups = ctx.Groups
.Include("Datapoints")
.Where(w => w.BusinessUnitID.Equals(businessUnitID))
.OrderBy(o => o.SortOrder).ToList();
groups.ForEach(g => g.Datapoints = g.Datapoints.OrderBy(d => d.SortOrder).ToList());
return groups;
}
Controller
public ActionResult Data()
{
ViewBag.Notification = string.Empty;
if (User.IsInRole(#"xxx\yyyyyy"))
{
List<Group> dataGroups = ctx.GetStaticMeasures(10);
return View(dataGroups);
}
else
{
throw new HttpException(403, "You do not have access to the data.");
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Data(List<Group> model)
{
ViewBag.Notification = string.Empty;
if (User.IsInRole(#"xxx\yyyyyy"))
{
if (ModelState.IsValid)
{
ctx.SaveChanges(model);
ViewBag.Notification = "Save Successful";
}
}
else
{
throw new HttpException(403, "You do not have access to save the data.");
}
return View(model);
}
Main view
#model List<Jmp.StaticMeasures.Models.Group>
<div class="row">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="large-12">
<div class="large-8 large-centered columns panel">
#foreach (var g in #Model)
{
<h2>#g.Name</h2>
foreach (var d in g.Datapoints)
{
#Html.Partial("Measures", d)
}
<hr />
}
<input type="submit" class="button" value="Save Changes"/>
</div>
</div>
}
</div>
Partial View
#model Jmp.StaticMeasures.Models.Datapoint
#Html.HiddenFor(d => d.ID)
#Html.HiddenFor(d => d.Name)
#Html.HiddenFor(d => d.SortOrder)
#Html.DisplayTextFor(d => d.Name)
#Html.EditorFor(d => d.StaticValue)
#Html.ValidationMessageFor(d => d.StaticValue)
Rendered Html showing consecutive IDs
As you've rightly noted, this is because you're using a partial. This is happening because Html.Partial has no idea that it's operating on a collection, so it doesn't generate the names for your form elements with your intention of binding to a collection.
However, the fix in your case appears to be fairly straightforward. Rather than using Html.Partial, you can simply change your partial into an EditorTemplate and call Html.EditorFor on that template instead. Html.EditorFor is smart enough to know when it's handling a collection, so it will invoke your template for each item in the collection, generating the correct names on your form.
So to do what you need, follow these steps:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates). The name is important as it follows a convention for finding templates.
Place your partial view in that folder. Alternatively, put it in the Shared\EditorTemplates folder.
Rename your partial view to Datapoint.cshtml (this is important as template names are based on the convention of the type's name).
Now the relevant view code becomes:
// Note: I removed # from Model here.
#foreach (var g in Model)
{
<h2>#g.Name</h2>
#Html.EditorFor(m => g.DataPoints)
<hr />
}
This ensures the separation of your views, as you had originally intended.
Update per comments
Alright, so as I mentioned below, the problem now is that the model binder has no way of associating a DataPoint with the correct Group. The simple fix is to change the view code to this:
for (int i = 0; i < Model.Count; i++)
{
<h2>#Model[i].Name</h2>
#Html.EditorFor(m => m[i].DataPoints)
<hr />
}
That will correctly generate the names, and should solve the model binding problem.
OP's addendum
Following John's answer I also included the missing properties on the Group table as HiddenFor's which game me the model back on the post.
#for (int i = 0; i < Model.Count(); i++)
{
#Html.HiddenFor(t => Model[i].ID)
#Html.HiddenFor(t => Model[i].BusinessUnitID)
#Html.HiddenFor(t => Model[i].SortOrder)
#Html.HiddenFor(t => Model[i].Name)
<h2>#Model[i].Name</h2>
#Html.EditorFor(m => Model[i].Datapoints)
<hr />
}
Update 2 - Cleaner solution
My advice for using an EditorTemplate for each DataPoint also applies to each Group. Rather than needing the for loop, again sprinkling logic in the view, you can avoid that entirely by setting up an EditorTemplate for Group. Same steps apply as above in terms of where to put the template.
In this case, the template would be Group.cshtml, and would look as follows:
#model Jmp.StaticMeasures.Models.Group
<h2>#Model.Name</h2>
#Html.EditorFor(m => m.DataPoints)
<hr />
As discussed above, this will invoke the template for each item in the collection, which will also generate the correct indices for each Group. Your original view can now be simplified to:
#model List<Jmp.StaticMeasures.Models.Group>
#using (Html.BeginForm())
{
// Other markup
#Html.EditorForModel();
}
Binder can't bind to list of objects if it is returned like this. Yes, partial is your problem. You need to specify a number within your form for ID's.
Do something like this:
// pseudocode
#model List<Jmp.StaticMeasures.Models.Group>
<div class="row">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="large-12">
<div class="large-8 large-centered columns panel">
for(int i; i<Model.Count; i++)
{
<h2>#g.Name</h2>
#Html.HiddenFor(d => Model[i].Id)
#Html.HiddenFor(d => Model[i].Name)
#Html.HiddenFor(d => Model[i].SortOrder)
#Html.DisplayTextFor(d => Model[i].Name)
#Html.EditorFor(d => Model[i].StaticValue)
#Html.ValidationMessageFor(d => Model[i].StaticValue)
<hr />
}
<input type="submit" class="button" value="Save Changes"/>
</div>
</div>
}
</div>
See more details about binding to a list in Haack's blog
You are getting a null model because of the way the model binder handles collections.
Your partial view is rendering those inputs as for example:
<input type="hidden" name="ID" value="1"/>
...
And then repeating that for each entry in your List<Group>. Unfortunately the model binder won't know how to handle that and you'll get a null value.
The way your inputs have to look is:
<input type="hidden" name="groups[0].ID" value="1"/>
...
<input type="hidden" name="groups[1].ID" value="2"/>
There can't be a break in the numbering. One way to get this is to rewrite the way you use the Html.xxxFor methods, e.g.: iterate over the list and do this:
#Html.HiddenFor(d => Model[i].Id)
Here are two resources that explain this in detail and provide yet other examples of how to make the model binder work with collections:
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/

Categories