This question already has answers here:
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 5 years ago.
I have an admin page in my MVC5 web application where a user is selected and upon clicking the proceed button a list of all roles with checkboxes appear (via Ajax). If a user is in any of these roles, the checkbox will be checked automatically.
I want to pass a List in my HttpPost method once the user checks/unchecks the boxes for each role. However, the parameter to my action method is null but there are values in Request.Form.
I don't understand why that is. Also do I really need to use #Html.HiddenFor() for each parameter in my viewmodel in order for things to work correctly?
RoleCheckBoxViewModel
public class RoleCheckBoxViewModel
{
[Display(Name = "Choose Role(s)")]
[Key]
public string RoleId { get; set; }
public string UserId { get; set; }
public string Name { get; set; }
[Display(Name="boxes")]
public bool IsChecked { get; set; }
}
RolesController Action
[HttpPost]
public ActionResult Update(List<RoleCheckBoxViewModel> list) //the model in my view is a List<RoleCheckBoxViewModel> so then why is it null?
{
//these next two lines are so that I can get to the AddToRole UserManagerExtension
var userStore = new UserStore<ApplicationUser>(_context);
var userManager = new UserManager<ApplicationUser>(userStore);
foreach (var item in Request.Form) //it's messy but I can see the data in this Form
{
System.Console.WriteLine(item.ToString());
}
//AddToRole for any new checkboxes checked
//RemoveFromRole any new checkboxes unchecked
//context save changes
return RedirectToAction("Index");
}
The AllRoles Partial View (A result of a previous AJAX call)
#model List<Core.ViewModels.RoleCheckBoxViewModel>
#using (Html.BeginForm("Update", "Roles", FormMethod.Post))
{
<p>
Select Roles
</p>
<table class="table">
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(model => item.Name)
#Html.CheckBoxFor(model => item.IsChecked)
</td>
</tr>
#Html.HiddenFor(model => item.RoleId)
#Html.HiddenFor(model => item.UserId)
#Html.HiddenFor(model => item.IsChecked)
#Html.HiddenFor(model => item.Name)
}
</table>
<input type="submit" value="Save" class="glyphicon glyphicon-floppy-save" />
}
The model binder in case of collections needs indexing on the name of the input controls to post in the controller action as collection. you can change your loop to use for loop instead and do indexing in helper methods and yes you will need to create hidden inputs for the properties to be posted if you don't want them to be edited by user.
You can change your loop code to be like following to get model correctly posted back :
#for(int i=0; i < Model.Count' i++)
{
<tr>
<td>
#Html.DisplayFor(model => Model[i].Name)
#Html.CheckBoxFor(model => Model[i].IsChecked)
</td>
</tr>
#Html.HiddenFor(model => Model[i].RoleId)
#Html.HiddenFor(model => Model[i].UserId)
#Html.HiddenFor(model => Model[i].IsChecked)
#Html.HiddenFor(model => Model[i].Name)
}
Hope it helps!
Related
This question already has answers here:
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 5 years ago.
I have object A which has a List<ObjectB>
ObjectB has several properties. Id, Mandatory, Name etc.
So I return a viewmodel (ObjectA) and have this in my razor:
#model ObjectA
<div>
<div>#Html.HiddenFor(m => ObjectA.ObjectC.ID)
<dl class="dl-horizontal">
<dt>
#Html.DisplayNameFor(model => ObjectA.ObjectC.Name)
</dt>
<dd>
#Html.DisplayFor(model => ObjectA.ObjectC.Name)
</dd>
</dl></div>
// display stuff from objectA
#using (Html.BeginForm())
{
foreach (var ft in ObjectA.ObjectB)
{
#Html.HiddenFor(c => ft.ID)
<div class="row">
<div class="col">
#if (ft.Mandatory)
{
#Html.CheckBoxFor(c => ft.Mandatory, new { id = ft.ID, disabled = "disabled" })
#Html.HiddenFor(c => ft.Mandatory)
}
else
{
#Html.CheckBoxFor(c => ft.Mandatory, new { id = ft.ID })
}
</div>
</div>
</div>
</div>
and in my Controller I tried as input parameter:
List<ObjectB> items
but it was null. Now I know I could try FormCollection which I did and found out that form.Get("ft.id") had the amount of items in de objectB list. same for mandatory. But I'd like it strong typed. Either:
1 object A with all subobjects of type B
2 a list/ienumerable of type objectB.
It's probably a small thing, but I can't see it right now.
edit my model:
public class ObjectA : BaseViewModel
{
public ObjectC DisplayOnly { get; internal set; }
public List<ObjectB> Features { get; set; }
}
My view: (see above)
My controller:
[Route("Details/{id:int}")]
[HttpPost]
public ActionResult Details(ObjectA vm)
{
if (ModelState.IsValid)
{
int hid = Convert.ToInt32(RouteData.Values["id"]);
}
}
With your current code, it will render the checkbox input elements with name attribute values set to ft.Mandatory. When the form is submitted, model binder has no idea where to map this to because it does not match with the view model property names/property hierarchy. For model binding to work, the names of input should match with the parameter class's property name.
You can use Editor Templates to handle this use case.
Create a folder called EditorTemplates in ~/Views/Shared or ~/Views/YourControllerName and create a new view and give it the same name as your class name which you are using to represent the data needed for the checkbox. In your case it will be ObjectB.cshtml
Now make this view strongly typed to OptionB and render the checkbox and hidden input.
#model OptionB
<div>
#Html.CheckBoxFor(x => x.Mandatory)
#Html.HiddenFor(c => c.Id)
</div>
Remember, the browser will not send the values of disabled form elements when the form is submitted.
Now in your main view, you can call EditorFor helper method.
#using (Html.BeginForm())
{
#Html.EditorFor(a=>a.OptionBList)
<button type="submit">Send form</button>
}
This will render the checkboxes with name attribute values like this (Assuming you have 3 items in OptionBList) , along with hidden inputs for checkboxes
Options[0].Mandatory
Options[1].Mandatory
Options[2].Mandatory
Now you can simply use OptionA as the parameter of your HttpPost action method
[HttpPost]
public ActionResult Create(OptionA model)
{
foreach(var item in model.OptionBList)
{
//check item.Mandatory and item.Id
}
//to do : return something
}
How do I get the entered values of textboxes to post to my controller with values. Thought of using arrays but have no idea where to start. Textboxes are dynamic due to each have counter number
#{
var counter = 0;
foreach (var addedProduct in Model)
{
counter++;
<tr class="default" data-ng-controller="deleteItemsController">
<td>#addedProduct.name</td>
<td>#addedProduct.price</td>
<td>
<input type="text" id="quantity_#counter" name="quantity_#counter"/>
</td>
</tr>
}
}
<tr>
<td><b>Total:</b>{{#ViewBag.total}}</td>
<td>
#using (Html.BeginForm("Index", "Payment"))
{
//how do i get quantity values//
<input type="submit" value="pay">
}
</td>
my model:
public class ProductVar
{
public int productID { get; set; }
public string name { get; set; }
public int price { get; set; }
public int quantity { get; set; }
public int total { get; set; }
}
I have not implemented method yet, trying to figure out how to post all values first.
You are pretty close. Read this blog post from Phil Haack and things will be clear how your input fields should be named. You will understand how model binding works in ASP.NET MVC as well.
Also you should place your <input> fields inside the HTML <form> or nothing will ever be sent to the server when this form gets submitted.
So start by modifying your view code so that you use strongly typed HTML helpers for generating your input fields. They will take car of properly naming them:
#using (Html.BeginForm("Index", "Payment"))
{
for (var i = 0; i < Model.Count; i++)
{
<tr class="default" data-ng-controller="deleteItemsController">
<td>#Html.DisplayFor(x => x[i].name)</td>
<td>#Html.DisplayFor(x => x[i].price)</td>
<td>
#Html.EditorFor(x => x[i].quantity)
</td>
</tr>
}
<tr>
<td>
<b>Total:</b>
#ViewBag.total
</td>
<td>
<input type="submit" value="pay" />
</td>
</tr>
}
Now you can have your controller action that will be triggered when the form is submitted:
[HttpPost]
public ActionResult Index(ProductVar[] model)
{
...
}
In this example only the quantity field of the model will be sent to the server because that's the only input field present inside the HTML form and so the only values sent to the server. If you want to bind the other values you might include them as hidden fields as well:
#Html.HiddenFor(x => x[i].name)
#Html.HiddenFor(x => x[i].price)
Of course in most cases this is bad practice because the user can manipulate those values. The best approach is to retrieve them from the db in your POST action using the same id that you used to retrieve them in the GET action that you used to render this form.
in my create view I want to give the user the possibility to create a list of objects (of the same type). Therefore I created a table in the view including each inputfield in each row. The number of rows respective "creatable" objects is a fixed number.
Lets say there is a class Book including two properties title and author and the user should be able two create 10 or less books.
How can I do that?
I don't know how to pass a list of objects (that are binded) to the controller. I tried:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ICollection<Book> bookList)
{
if (ModelState.IsValid)
{
foreach(var item in bookList)
db.Books.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(articlediscounts);
}
And in the view it is:
<fieldset>
<legend>Book</legend>
<table id="tableBooks" class="display" cellspacing="0" width="100%">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < 10 ;i++ )
{
<tr>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
</td>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Author)
#Html.ValidationMessageFor(model => model.Author)
</div>
</td>
</tr>
}
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
As booklist is null, it doesn't work and I don't know how to put all created objects in this list.
If you have any suggestions I would be very thankful.
Scott Hanselman has some details on passing arrays to MVC control binding: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Which is essentially: ensure your controls have the correct names: using an index for lists
Change your for loop to something like:
#for (int i = 0; i < 10 ; i++)
{
<tr>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Title" />
</div>
</td>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Author" />
</div>
</td>
</tr>
}
this will then bind to your post action automatically.
[HttpPost]
public ActionResult Create(IList<Book> bookList)
You can then show/hide these as required or use js/jquery to add them dynamically
Edit: As correctly observed by Stephen Muecke, the above answer only regards the binding from the form+fields to the HttpPost, which appears to be the emphasis of the question.
The post action in the original post is not compatible with the view. There's quite a bit of missing code in the OP that may or may not be relevant, but worth observing that if your view is for a single model, then your fail code on ModelState.IsValid needs to return a single model or your view needs to be for an IList (or similar), otherwise you won't get server-side validation (but you can still get client-side validation if you manually add it to the <input>s)
The fact you use #Html.EditorFor(model => model.Title) suggests that you have declared the model in the view as
#model yourAssembly.Book
Which allows to to post back only one Book so the POST method would need to be
public ActionResult Create(Book model)
Note that you current implementation create inputs that look like
<input id="Title" name="Title" ... />
The name attributes do not have indexers (they would need to be name="[0].Title", name="[1].Title" etc.) so cannot bind to a collection, and its also invalid html because of the duplicate id attributes.
If you want to create exactly 10 books, then you need initialize a collection in the GET method and pass the collection to the view
public ActionResult Create()
{
List<Book> model = new List<Book>();
for(int i = 0; i < 10;i++)
{
model.Add(new Book());
}
return View(model);
}
and in the view
#model yourAssembly.Book
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.TextBoxFor(m => m[i].Title)
#Html.ValidationMessageFor(m => m[i].Title)
.... // ditto for other properties of Book
}
<input type="submit" .. />
}
which will now bind to your collection when you POST to
public ActionResult Create(List<Book> bookList)
Note the collection should be List<Book> in case you need to return the view.
However this may force the user to create all 10 books, otherwise validation may fail (as suggested by your use of #Html.ValidationMessageFor()). A better approach is to dynamically add new Book items in the view using either the BeginCollectionItem helper method (refer example) or a client template as per this answer.
You'd need to send a JSON object that has the list of books in it. So the first thing is to create a Model class like this:
public class SavedBooks{
public List<Book> Books { get; set; }
}
Then the Book class would have to have these 2 props:
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
Next, change your controller to use this model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SavedBooks model)
Then create a javascript method (using jQuery) to create a JSON object that matches the structure of the controllers SavedBooks class:
var json = { Books: [ { Title: $('#title_1').val(), Author: $('#Author_1').val() } ,
{ as many items as you want }
]
};
$.ajax(
{
url: "/Controller/Create",
type: "POST",
dataType: "json",
data: json
});
I have got this problem that I am having a difficulty to solve. I am creating a page where the user will be presented with a list of items (Product Types). Each item will have a dropdown list next to it so that the user can make appropriate selection to create a mapping. After making selection then the user submits the form, and the value will be written to the database.
The problem is that when it is submitted, I am not getting any values back. Specifically, 'Mappings' is empty in the model that is returned by the POST action. The GET action works fine. The following is the essence of what I have written:
Model:
public class ProductTypeMappingViewModel
{
//this is empty in the POST object
public List<ProductTypeMapping> Mappings { get; set; }
public ProductTypeMappingViewModel()
{
Mappings = new List<ProductTypeMapping>();
}
public ProductTypeMappingViewModel(string db)
{
//use this to populate 'Mappings' for GET action
//works fine
}
public void UpdateDB()
{
//to be called on the object
//returned from POST action
foreach(var mapping in Mappings)
{
//Mappings is always empty after POST
//Suppose to add to db
}
}
}
public class ProductTypeMapping
{
public string ProductTypeName { get; set; }
public int SelectedStandardProductTypeKey { get; set; }
public SelectList StandardProductTypes { get; set; }
public ProductTypeMapping()
{
StandardProductTypes = new SelectList(new List<SelectListItem>());
}
public int GetSelectedProductTypeKey() { //return selected key}
public string GetSelectedProductTypeName() { //return selected name}
}
View:
#model CorporateM10.Models.ProductTypeMappingViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
<table class="table">
#foreach (var dept in Model.Mappings)
{
<tr>
<td>
#Html.DisplayFor(model => dept.ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
<div>
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
}
Any insight will be greatly appreciated.
foreach here causes select element in final HTML to have incorrect name attribute. Thus nothing is posted to the server. Replace this with for loop:
<table class="table">
#for (int i=0; i<Model.Mappings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Mappings[i].ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey, model.Mappings[i].StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
As #Andrei said the problem relies on the name attribute.
But to add a little bit to his answer, here's the parameter names in the request that the default model binder expects for your case.
Mappings[0].SelectedStandardProductTypeKey
Mappings[1].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
...
Without any breaks in the numbering, i.e.:
Mappings[0].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
Won't work because of the missing Mapping[1]...
When you use the dropdown helper like this:
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
It generates an input with name="SelectedStandardProductTypeKey" (you need it to be Mappings[0].SelectedStandardProductTypeKey)
If you use a for loop and use the dropdown helper like this:
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey
You'll get the input with the correct name.
Any parameter in the request for which the model binder cannot find a property in the model, it will ignore, that's why the Mappings property is null in your case.
Here are two great resource that explain all this (and that provide alternative ways to represent collections that might be useful if you can't a the for loop to generate a numbered index without breaks):
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
I'm writing a view that displays a list of managers. The managers have checkboxes next to their name to select them to be removed from the manager list. I am having problems binding the form submission back to my view model. Here's what the page looks like:
Here's the ViewModel for the page.
public class AddListManagersViewModel
{
public List<DeleteableManagerViewModel> CurrentManagers;
}
And here's the sub-ViewModel for each of the DeleteableManagers:
public class DeleteableManagerViewModel
{
public string ExtId { get; set; }
public string DisplayName { get; set; }
public bool ToBeDeleted { get; set; }
}
This is the code for the main View:
#model MyApp.UI.ViewModels.Admin.AddListManagersViewModel
<div class="row">
<div class="span7">
#using (Html.BeginForm("RemoveManagers","Admin"))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>System Managers</legend>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.CurrentManagers)
</tbody>
</table>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Delete Selected</button>
</div>
}
</div>
</div>
And this is the EditorTemplate I've created for DeleteableManagerViewModel:
#model MyApp.UI.ViewModels.Admin.DeleteableManagerViewModel
<tr>
<td>#Html.DisplayFor(model => model.DisplayName)</td>
<td>
#Html.CheckBoxFor(model => model.ToBeDeleted)
#Html.HiddenFor(model => model.ExtId)
</td>
</tr>
But when I submit the form to the controller the model comes back null! this is what I want it to do:
[HttpPost]
public virtual RedirectToRouteResult RemoveManagers(AddListManagersViewModel model)
{
foreach (var man in model.CurrentManagers)
{
if (man.ToBeDeleted)
{
db.Delete(man.ExtId);
}
}
return RedirectToAction("AddListManagers");
}
I tried following along this post: CheckBoxList multiple selections: difficulty in model bind back but I must be missing something....
Thanks for your help!
Hmm. I think this is ultimately the problem; here's what you're posing:
CurrentManagers[0].ToBeDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].ExtId=X00405982144
Your model is an AddListManagersViewModel that has a collection of CurrentManagers. So, you're posting an array of DeleteableManagerViewModel, which isn't getting bound to the "wrapper" model. You can try changing the model parameter to
params DeleteableManagerViewModel[] model
I don't ever use the EditorFor extensions, though, so I'm just guessing...