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
}
Related
In my app I want to make model that can have dynamic list of attributes:
Provider class
using System;
using System.Collections.Generic;
namespace DynamicForms.Models
{
public class Provider
{
public String Name { get; set; }
public List<Attribute> Attributes { get; set; }
public Provider()
{
Attributes = new List<Attribute>();
}
}
}
My controller has two methods. One to add more attributes to current model and one to save Provider model. Method to add more attributes looks like this:
[HttpPost]
// Add attribute to current provider model
public IActionResult Index(Provider provider)
{
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
And my view looks like this:
#model Provider
#using (Html.BeginForm())
{
<div>
#Html.TextBoxFor(m => m.Name)
<input type="submit" value="Add attribute" formmethod="post"/>
</div>
<div>
#foreach ( var attribute in Model.Attributes)
{
<div>
#Html.TextBoxFor(a => attribute.Name)
#Html.TextBoxFor(a => attribute.Value)
</div>
}
</div>
<div>
<input type="submit" value="Save form" formaction="Provider/Save" formmethod="post"/>
</div>
}
When I press "Add attribute" button attribute is added and a row of input fields is appearing. However when i press the button one more time nothing happens. No another row is added. Model attributes count is still 1. I've been looking for solutions all over the web but couldn't find anything that fixes my problem. How can I make form fields be added dynamically to model and displayed in vieW?
Try looping over your List with an index instead of foreach, e.g.
#for (var i = 0; i < Model.Attributes.Count; i++)
{
<div>
#Html.TextBoxFor(a => Model.Attributes[i].Name)
#Html.TextBoxFor(a => Model.Attributes[i].Value)
</div>
}
You want the names to be formatted something like this Attributes[0].Name, etc.
You wrote a method for Post, but you wrote a method for get.
Get method
public IActionResult Index()
{
Provider provider = new Provider();
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
Post metod
[HttpPost]
// Add attribute to current provider model
public IActionResult Index(Provider provider)
{
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
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!
The Model:
class Address
{
public string City { get; set; }
public string Zip { get; set; }
}
The Controller:
[HttpPost]
public ActionResult GetAddress(Address model)
{
if (!String.IsNullOrEmpty(model.Zip))
{
model.City = GetCityByZip(model.Zip);
}
return View(model);
}
The View:
<div class="formrow">
#Html.LabelFor(model => model.City)
#Html.TextBoxFor(model => model.City)
#Html.ValidationMessageFor(model => model.City)
</div>
<div class="formrow">
#Html.LabelFor(model => model.Zip)
#Html.TextBoxFor(model => model.Zip)
#Html.ValidationMessageFor(model => model.Zip)
</div>
The problem is whenever the city is being modified, it never gets reflected on the view. During debugging, the model.City contains the correct value but it doesn't show up on view. Even something as simple as #Html.TextBoxFor(model => model.City) doesn't display the correct model.City value.
HtmlHelpers get the model values from the model state and not the model when you update and return the model. In order to update and return the model, add this line of code in your post method:
ModelState.Clear();
or you could set the value of city in the ModelState itself:
ModelState["City"].Value = GetCityByZip(model.Zip);
As Tommy noted, this is, somewhat counterintuitively, the correct behavior since form data submitted on post gets first priority when binding the data to the returned view. This makes some sense as the user is likely to have made a validation error when re-returning the same view and gets to resume their form entry as is without the problems of losing form input when restoring a page
One other option is to manually insert the value for the input
So instead of this:
#Html.TextBoxFor(model => model.City)
Do this instead:
<input type="text" name="City" value="#Model.City" />
* which will grab the value directly off the model
Or even better:
<input type="text" value="#Model.City"
name="#Html.NameFor(model => model.City)"
id="#Html.IdFor(model => model.City)" />
*Note: this won't bring in data-val attributes. If you're using them on this property for client side validation, you'll need to build list of data validation attributes for a given element
Additional Resources
HiddenFor not getting correct value from view model
HTML.HiddenFor is not updating on a postback
ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes
Based on Darin's answer to my question Ho to display multiple checkbox selection based on user's selection from dropdown?
I am displaying multiple checkboxes based on dropdown selection.
Now, once the user post the form (with multiple inputs) that i have on my page, i collect all the data using FormCollection. And the problem i have is how can i pull those selected checkbox values from formcollection? The number of checkbox will change on different selection from the drop-down, so i think requesting each checkbox value will not work.
Can anyone help me with this problem.
The flow is as shown below:
Properties in Model
public class Subcategory
{
public string Name { get; set; }
public int ID { get; set; }
public bool Flag { get; set; }
}
Displaying PartialView in actual view where other form inputs are there:
<div id="checkboxlist">
#if (Model.SubCategories != null)
{
#Html.Partial("SubCategories", Model.SubCategories)
}
</div>
PartialView SubCategories.cshtml
#model IEnumerable<MyProject.Entities.Subcategory>
#{
// we change the HTML field prefix so that input elements
// such as checkboxes have correct names in order to be able
// to POST the values back
ViewData.TemplateInfo.HtmlFieldPrefix = "checkboxlist";
}
<span>subcategory</span>
<div id="subcategories" style="margin-left: 130px;margin-top: -20px;" data-role="fieldcontain">
<fieldset data-role="controlgroup">
#Html.EditorForModel()
</fieldset>
</div>
EditorTemplates Subcategory.cshtml
#model MyProject.Entities.Subcategory
<div class="editor-label">
#Html.CheckBoxFor(c => c.Flag, new { type = "checkbox" })
<label for="#Model.ID">#Model.Name</label>
#Html.HiddenFor(c => c.Flag)
#Html.HiddenFor(c => c.ID)
#Html.HiddenFor(c => c.Name)
</div>
jquery to display checkboxes based on dropdown selection:
$('#Category').change(function () {
var subcategoriesUrl = $(this).data('subcategoriesurl');
var categoryId = $(this).val();
$('#checkboxlist').load(subcategoriesUrl, { category: categoryId });
});
Don't use FormCollection. That's weakly typed. Use view models. Like this:
[HttpPost]
public ActionResult Foo(MyViewModel model)
{
// model.BusinessSubCategories should contain a list of Subcategory
// where for each element you could use the Flag property to see if
// it was selected or not
...
}
Also notice that you have an inconsistency between the field prefix that you are using in your partial:
ViewData.TemplateInfo.HtmlFieldPrefix = "checkboxlist";
and the view model collection property: Model.BusinessSubCategories. So make sure you fix the prefix to use the correct property name if you want the default model binder to be able to populate this property when you post back.
I have a class that looks like this:
public class UserListVM
{
public SearchModel SearchModel { get; set; }
public PagedList<User> Users { get; set; }
}
public class SearchModel
{
public string Text { get; set; }
/* other properties */
}
I send UserListVM to my view but the action accepts SearchModel:
public ActionResult Search(SearchModel filter)
{
UserListVM model = new UserListVM();
model.Users = userService.GetUsers(filter);
model.SearchModel = filter;
return View(model);
}
My view is:
#model UserListVM
<form>
#Html.TextBoxFor(m => Model.SearchModel.Text)
</form>
But this generates:
<input id="SearchModel_Text" name="SearchModel.Text" type="text" value="">
Which sends UserListVM to the action instead of SearchModel. How can I get it to generate this:
<input id="Text" name="Text" type="text" value="">
#Html.TextBoxFor(m => m.SearchModel.Text, new { id = "Text" })
Utilize the overloaded TextBoxFor() method that takes a second object parameter (called htmlAttributes). Here you can specify HTML attributes to apply to the DOM element you are currently utilizing (in this case, your input element).
Edit: I believe your lambda expression is wrong. Change:
#Html.TextBoxFor(m => Model.SearchModel.Text)
To
#Html.TextBoxFor(m => m.SearchModel.Text)
// htmlAttributes omitted to show the issue
Edit Edit: it turns out that even with a specified name attribute, it will be rendered according to what the form is requiring for a POST to the necessary field(s).
Edit Edit Edit: Try to be explicit with FormExtensions.BeginForm():
#using (Html.BeginForm("Search", "YourController", FormMethod.Post, null))
{
#Html.TextBoxFor(m => m.SearchModel.Text)
}
Use this as a substite of your <form /> element.
Create a partial view for your SearchModel, and call it using Html.Partial. Then, from within that partial view, do all of the EditorFor/TextBoxFor Extensions
Your view - UserList.cshtml:
#model UserListVM
#using (Html.BeginForm())
{
#Html.Partial("Search", Model.SearchModel)
}
Your view - Search.cshtml:
#model SearchModel
#Html.TextAreaFor(m => m.Text)
Assumming there is more the the View than you have shown, why not just have your Search method take the UserListVM model. It will just contain a null reference to the users, so there is no extra data sent in the post.
Try doing it manually like this:
#Html.TextBox("Text", Model.SearchModel.Text)