I'm trying to create a view that contains a list of checkboxes that is dynamically created from a database, and then retrieve the list of selected ones when the form is posted back.
My EF model contains a class:
public class ItemIWouldLikeACheckboxFor {
public int Id { get; set; }
public string Description { get; set; }
}
I have a view model that contains a list of these:
public class PageViewModel {
// various other properties
public List<ItemIWouldLikeACheckboxFor> checkboxList { get; set; }
}
My controller get method:
public ActionResult Create() {
var viewModel = new PageViewModel();
viewModel.checkboxList = db.ItemIWouldLikeACheckboxFors.ToList();
return View(viewModel);
}
My view:
<% using (Html.BeginForm()) { %>
<%-- other stuff here... %>
<% foreach (var item in checkboxList) { %>
<%: Html.CheckBox( <!-- what exactly ?????? -->) %>
<% } %>
<%-- other stuff here...%>
<input type="submit" />
<% } %>
My controller post method:
[HttpPost]
public ActionResult Create(PageViewModel viewModel) {
// do stuff with other fields
// I would like to do something like:
foreach (var item in selectedCheckBoxes) {
// do stuff
}
}
I can't seem to get it to work. My basic questions are mixed in as comments in the code snippets, but to recap:
Is my view model OK? (do I need to add anything to capture the selected ones as opposed to simply the list to display?)
What exactly should I put in the view to render each check box?
How do I access the selected checkboxes in the controller after the post?
Have you seen: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx?
We basically wrote our own control to render the HTML like
<label for="Products"> Select Products </label>
<ul class="checkBoxList">
<li>
<input type="hidden" value="0" name="Products.Index">
<input type="checkbox" value="3424" name="Products[0].Id" id="Products0">
<label for="Products0">iPod touch 3rd Generation</label>
</li>
<li>
<input type="hidden" value="1" name="Products.Index">
<input type="checkbox" value="3123" name="Products[1].Id" id="Products1">
<label for="Products1">Creative Zen</label>
</li>
</ul>
</div>
Model Looks Ok, we wrote a custom helper, so our aspx pages look like:
<%= Html.DropDownFor(m=>m.products) %>
If you follow phil haacks post, your model should automatically bind in your controller.
Also a good answer in this question: CheckBoxList multiple selections: difficulty in model bind back
It has a solution that uses a custom Editor Template.
Related
so I have an MVC Asp.net app that is having issues. Essentially, I have a View that contains a form, and its contents are bound to a list of objects. Within this loop, it loads PartialView's with the items being looped over. Now everything works up till the submittion of the form. When it gets submitted, the controller is sent a null list of objects. The code below demonstates the problems.
Parent View:
#model IEnumerable<PlanCompareViewModel>
#using (Html.BeginForm("ComparePlans", "Plans", FormMethod.Post, new { id = "compareForm" }))
{
<div>
#foreach (var planVM in Model)
{
#Html.Partial("_partialView", planVM)
}
</div>
}
_partialView:
#model PlanCompareViewModel
<div>
#Html.HiddenFor(p => p.PlanID)
#Html.HiddenFor(p => p.CurrentPlan)
#Html.CheckBoxFor(p => p.ShouldCompare)
<input type="submit" value="Compare"/>
</div>
And these are the classes for the above code:
PlanViewModel:
public class PlansCompareViewModel
{
public int PlanID { get; set; }
public Plan CurrentPlan { get; set; }
public bool ShouldCompare { get; set; }
public PlansCompareViewModel(Plan plan)
{
ShouldCompare = false;
PlanID = plan.PlanId;
CurrentPlan = plan;
}
public PlansCompareViewModel()
{
// TODO: Complete member initialization
}
public static IEnumerable<PlansCompareViewModel> CreatePlansVM(IEnumerable<Plan> plans)
{
return plans.Select(p => new PlansCompareViewModel(p)).AsEnumerable();
}
}
Controller:
public class PlansController : MyBaseController
{
[HttpPost]
public ActionResult ComparePlans(IEnumerable<PlanCompareViewModel> model)
{
//the model passed into here is NULL
}
}
And the problem is in the controller action. As far as I am aware, it should be posting an enumerable list of PlanCompareViewModels, yet it is null. When in inspect the post data being sent, it is sending the correct params. And if I were to change 'IEnumerable' to 'FormCollection', it contains the correct values. Can anyone see why the binder is not creating the correct object? I can get around this using javascript, but that defeats the purpose! Any help would be greatly appreciated!
Your model is null because the way you're supplying the inputs to your form means the model binder has no way to distinguish between the elements. Right now, this code:
#foreach (var planVM in Model)
{
#Html.Partial("_partialView", planVM)
}
is not supplying any kind of index to those items. So it would repeatedly generate HTML output like this:
<input type="hidden" name="yourmodelprefix.PlanID" />
<input type="hidden" name="yourmodelprefix.CurrentPlan" />
<input type="checkbox" name="yourmodelprefix.ShouldCompare" />
However, as you're wanting to bind to a collection, you need your form elements to be named with an index, such as:
<input type="hidden" name="yourmodelprefix[0].PlanID" />
<input type="hidden" name="yourmodelprefix[0].CurrentPlan" />
<input type="checkbox" name="yourmodelprefix[0].ShouldCompare" />
<input type="hidden" name="yourmodelprefix[1].PlanID" />
<input type="hidden" name="yourmodelprefix[1].CurrentPlan" />
<input type="checkbox" name="yourmodelprefix[1].ShouldCompare" />
That index is what enables the model binder to associate the separate pieces of data, allowing it to construct the correct model. So here's what I'd suggest you do to fix it. Rather than looping over your collection, using a partial view, leverage the power of templates instead. Here's the steps you'd need to follow:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
Create a strongly-typed view in that directory with the name that matches your model. In your case that would be PlanCompareViewModel.cshtml.
Now, everything you have in your partial view wants to go in that template:
#model PlanCompareViewModel
<div>
#Html.HiddenFor(p => p.PlanID)
#Html.HiddenFor(p => p.CurrentPlan)
#Html.CheckBoxFor(p => p.ShouldCompare)
<input type="submit" value="Compare"/>
</div>
Finally, your parent view is simplified to this:
#model IEnumerable<PlanCompareViewModel>
#using (Html.BeginForm("ComparePlans", "Plans", FormMethod.Post, new { id = "compareForm" }))
{
<div>
#Html.EditorForModel()
</div>
}
DisplayTemplates and EditorTemplates are smart enough to know when they are handling collections. That means they will automatically generate the correct names, including indices, for your form elements so that you can correctly model bind to a collection.
Please read this: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
You should set indicies for your html elements "name" attributes like planCompareViewModel[0].PlanId, planCompareViewModel[1].PlanId to make binder able to parse them into IEnumerable.
Instead of #foreach (var planVM in Model) use for loop and render names with indexes.
In my case EditorTemplate did not work. How I did -
ViewModel file -
namespace Test.Models
{
public class MultipleFormViewModel
{
public int Value { get; set; }
public string Title { get; set; }
}
}
Main View (cshtml) file -
#model List<MultipleFormViewModel>
#{
var list = new List<MultipleFormViewModel>();
ViewData["index"] = 0;
}
#using (Html.BeginForm("SaveDataPoint", "MultipleForm", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
for (int i = 0; i < 3; i++)
{
var dataPoint = new MultipleFormViewModel();
list.Add(dataPoint);
ViewData["index"] = i;
#Html.Partial("_AddDataPointFormPartial", ((List<MultipleFormViewModel>)list)[i], ViewData)
}
<input type="submit" value="Save Data Points" />
}
Partial View (under Shared folder) file -
#model MultipleFormViewModel
#{
var index = ViewData["index"];
}
<div>
<div>
<label>Value:</label>
<input asp-for="Value" name="#("["+index+"].Value")" id="#("z"+index+"__Value")" />
</div>
<div>
<label>Title:</label>
<input asp-for="Title" name="#("["+index+"].Title")" id="#("z"+index+"__Title")" />
</div>
</div>
And Finally Controller -
[HttpPost]
public IActionResult SaveDataPoint(List<MultipleFormViewModel> datapoints)
{
Console.WriteLine(datapoints.Count);
//Write your code
return Content("hello..");
}
I have MVC application with a model like this :
public class ListOfMyModel
{
public List<MyModel> MyModels { get; set; }
public Guid MyID { get; set; }
}
public class MyModel
{
// Some code like :
public string MyString { get; set; }
}
And my post method in my controller is like this :
[HttpPost]
public ActionResult EditMe(ListOfModel myList)
{
try
{
if (ModelState.IsValid)
{
List<MyModel> myModels = myList.MyModels;
foreach (MyModel model in myModels)
// Some code
return RedirectToAction("Index");
}
catch
{
// Some code
return View(myList)
}
return View(myList);
}
And my view : ( I use Kendo UI ) ( P.S : Some code has been stripped away and replaced by comment code )
#model MyApplication.Web.Models.ListOfMyModel
#{
ViewBag.Title = MyTitle;
Layout = "~/Views/Shared/_MyLayout.cshtml";
}
<div class="span1"></div>
<div class="span8">
<div id="list-wrapper">
<div class="k-content">
<form id="form" class="form-horizontal well span8 offset2" method="post" action="#Url.Action("EditMe")">
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/kendo/2013.1.514/kendo.web.min.js")"></script>
<script src="#Url.Content("~/Scripts/kendo/2013.1.514/kendo.aspnetmvc.min.js")"></script>
<div class="offset2 span2">
<fieldset>
<legend> My title </legend>
<p>Some code :</p>
#Html.HiddenFor(m => m.MyID)
#for (int i = 0; i < Model.MyModels.Count; i++)
{
// Some code
<div class="control-group">
<label class="control-label">MyText : </label>
<div class="controls">
#(Html.Kendo().DropDownListFor(c => Model.MyModels[i].MyString)
.DataTextField("Text")
.DataValueField("Value")
.DataSource(dataSource => dataSource
.Read(read => read.Action("GetSomeThings", "MyController"))
)
.Value(Model.MyModels[i].MyString)
)
</div>
</div>
}
<div class="form-actions">
<button type="submit" class="btn btn-primary">Validate</button>
</div>
</fieldset>
</div>
</form>
</div>
</div>
</div>
But the problem is that when I push the submit button in my view, the method of my controller is called with all the data expected ( saw in Chrome ) but in this method, all of the model is null : The ID and the list... I don't known where the problem is ?
Thank you all for reading and trying to understand this, if you want more informations please tell me.
The MyId should be correctly received with your existing code.
The model binder can only match the value of inputs whose name have a matching property in the form. I.e. if you have an input like this <input name='bikes' ...> then your model should have a property Bikes so that the model binder can transfer the value from the input to the property. In your case you're creating input with dynamic names that doesn't have a matching property name. (NOTE: thei referes to the model you're using as the action parameter).
The farthest you can go is giving a series of input elements the same name, i.e. several elements like <input name='categories' ...> and receive it in an array parameter like string[] categories or a model which has a property like string[] Categories {get;set;}
In short, you have to redesign your model and view. If you used a List<string> instead of a List<MyModel>, and the view had a fixed name for the dropdow lists, like DropDownListFor(c => Model.MyModels, then the model binder would fill the MyModels property with the list of selected strings in each drop down list.
Hint: you can use a model for the View and receive a different model (or series of parameters) in the Action. In this way you can send more information to render the View, and receive a post with the essential data to process the user input.
See my answer to this question for alternatives. It explains something similar to this question.
I have an MVC view
<%# Page Language="C#" MasterPageFile="PathToMaster" Inherits="System.Web.Mvc.ViewPage<ModelData>" %>
and I have a form with HTML markup for a set of checkboxes:
<label for="MyCheckbox">Your choice</label>
<input type="checkbox" id="Option1" class="checkbox" name="MyCheckbox" value="Option one" />
<label for="Option1">Option one</label><br />
<input type="checkbox" id="Option2" class="checkbox" name="MyCheckbox" value="Option two" />
<label for="Option2">Option two</label><br />
and I have a controller-action pair
class MyController : Controller {
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult RequestStuff( ModelData data )
{
}
}
and that action is invoked when the form is submitted.
How do I map the checkboxes onto members of ModelData (and what members I have to add to ModelData) so that when the form is submitted data stores information on which checkboxes are checked?
OK, this one will be for MVC3, but - save for syntax changes - should work in MVC2 too. The approach is essentially the same.
First of all, you should prepare an appropriate (view)model
public class MyViewModel
{
[DisplayName("Option 1")]
public bool Option1 { get; set; }
[DisplayName("Option 2")]
public bool Option2 { get; set; }
}
Then you pass this model to the view you're showing (controller):
public ActionResult EditMyForm()
{
var viewModel = new MyViewModel()
return View(viewModel);
}
with the form:
#model MyViewModel
#using( Html.BeginForm())
{
#Html.Label("Your choice")
#Html.LabelFor(model => model.Option1) // here the 'LabelFor' will show you the name you set with DisplayName attribute
#Html.CheckBoxFor(model => model.Option1)
#Html.LabelFor(model => model.Option2)
#Html.CheckBoxFor(model => model.Option2)
<p>
<input type="submit" value="Submit"/>
</p>
}
Now here the HTML helpers (all the CheckBoxFor, LabelFor, EditorFor etc) allow to bind the data to the model properties.
Now mind you, an EditorFor when the property is of type bool will give you the check-box in the view, too. :)
And then, when you submit to the controller, it will auto-bind the values:
[HttpPost]
public ActionResult EditMyForm(MyViewModel viewModel)
{
//And here the view model's items will be set to true/false, depending what you checked.
}
First you define SelectList for Options. This will be used just to render checkboxes
public IList<SelectListItem> OptionsSelectList { get; set; }
Than, you define model that will hold value of single chosen option after post
public class ChooseOptionViewModel
{
public int OptionIdentifier { get; set; } //name or id
public bool HasBeenChosen { get; set; } //this is mapped to checkbox
}
Then IList of those options in ModelData
public IList<ChooseOptionViewModel> Options { get; set; }
And finally, the view
#for (int i = 0; i < Model.OptionsSelectList.Count(); i++)
{
<tr>
<td class="hidden">
#Html.Hidden("Options[" + i + "].OptionIdentifier", Model.OptionsSelectList[i].Value)
</td>
<td>
#Model.OptionsSelectList[i].Text
</td>
<td>
#Html.CheckBox("Options[" + i + "].HasBeenChosen", Model.Options != null && Model.Options.Any(x => x.OptionIdentifier.ToString().Equals(Model.OptionsSelectList[i].Value) && x.HasBeenChosen))
</td>
</tr>
}
After post, you just inspect Options.Where(x => x.HasBeenChosen)
This is full-functional, and it will allow you to redisplay view when validation errors occur, etc. This seems a bit complicated, but I haven't come up with any better solution than this.
This is my model:
public class IndexViewModel
{
public FilterConditions conditions { get; set }
public IEnumerable<SelectListItem> Countries { get; set }
}
public class FilterConditions
{
public string condition11 { get; set }
// ...
}
And I have an Index action method like so:
public ActionResult Index()
{
var model = new IndexViewModel();
// fill the model here with default values
return View(model);
}
The view renders a form with the filterconditions as input types.
Now I want the post back from that form be handled by this action method:
[HttpPost]
public ActionResult Index(FilterConditions model)
{
// do some magic with model and return another view here
}
and this actually works (I put a breakpoint in the method, and it gets called), but the properties of my model are always empty (default values), while they should contain the values which were posted by the form.
When I modify the action method like this:
[HttpPost]
public ActionResult Index(IndexViewModel model)
{
// do some magic with model.conditions and return another view here
}
It all works like it should, but this is not "right" (IMHO), as I don't need the ´Countries´ list on return, I only need the selected country (which is one of the conditions).
What is a nice (best practice) way to make this work without having to take the whole original viewmodel as an input parameter?
Btw, I'm using ASP.NET MVC 2 (I don't think it really matters, as I think it's the same problem in MVC 3, but I'm not entirely sure of that).
(I have been looking around the internet for "best practices" regarding dropdownlists and viewmodels within asp.net mvc, but the different recommendations I found didn't really line up with each other, and a lot is already outdated as well. I didn't find an "official" best practice around this. I hope I'm going in the right direction (having the list as part of my viewmodel), feel free to correct me on this matter if I'm not. Also feel free to point me to "endorsed best practices" about this if you know of any.)
Update:
I found out that I can use the [Bind] attribute with a Prefix of "filterconditions". And this indeed works for this view. But my original problem (I admit, it was not included in my question) is not solved.
It happens that this particular action method is also called from another view (it is an ajax call) where it doesn't have that prefix, in that case it doesn't work any more now. Any suggestions?
I've found the solution.
Apparently, when I use the same name for the parameter variable as the name of the type (the case doesn't have to match), like this:
[HttpPost]
public ActionResult Index(FilterConditions filterConditions)
{
// do some magic with model and return another view here
// now the filterConditions variable actually contains values!
}
Everything works like it should (the values of my filterConditions are not empty/null anymore). Apparently, the default modelbinder uses the name of the parameter as the potential prefix for the binding.
I'm glad I found out, but it would be nice if this is more clearly documented somewhere. It's not obvious at all.
Edit:
On request: this is the code in my view (aspx):
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyProject.Models.IndexViewModel>" %>
<%-- ... more stuff here ... --%>
<% using (Html.BeginForm())
{%>
<%= Html.ValidationSummary(true)%>
<fieldset>
<div class="editor-label">
<%= Html.LabelFor(model => model.FilterConditions.Country)%>
</div>
<div class="editor-field">
<%= Html.DropDownListFor(model => model.FilterConditions.Country, Model.Countries)%>
<%= Html.ValidationMessageFor(model => model.FilterConditions.Country)%>
</div>
<div class="editor-label">
<%= Html.LabelFor(model => model.FilterConditions.Make)%>
</div>
<div class="editor-field">
<%= Html.TextBoxFor(model => model.FilterConditions.Make)%>
<%= Html.ValidationMessageFor(model => model.FilterConditions.Make)%>
</div>
<%-- ... more fields inserted here ... --%>
<p>
<input type="submit" value=" Search... " />
</p>
</fieldset>
<% } %>
Hi fretje: Now I can use your way to solve your problem, First I have two Models "IndexViewModel" & "Index", and the DropDownList(it's doesn't matter, just offer the DropDown items):
public class IndexViewModel : Index
{
//public int value { get; set; }
public List<System.Web.Mvc.SelectListItem> items { get; set; }
}
public class Index
{
public int value { get; set; }
}
class DropDownList
{
public List<System.Web.Mvc.SelectListItem> GetDropDownList()
{
List<System.Web.Mvc.SelectListItem> result = new List<System.Web.Mvc.SelectListItem>();
result.Add(new System.Web.Mvc.SelectListItem
{
Value = "1",
Text = "Apple"
});
result.Add(new System.Web.Mvc.SelectListItem
{
Value = "2",
Text = "Milk"
});
return result;
}
}
And two Controllers is Test() and Test(Models.Index), I pass the IndexViewModel and postback IndexModel:
public ActionResult Test()
{
var result =
new Models.IndexViewModel
{
value = 1,
items = new Models.DropDownList().GetDropDownList()
};
return View(result);
}
[HttpPost]
public ActionResult Test(Models.Index posatback)
{
return View();
}
The View of Test() is:
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Fields</legend>
<div class="editor-field">
<%: Html.DropDownListFor(m=>m.value, Model.items )%>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
that's work! thank you fretje, I learned one more tech. : )
Maybe you can try
[HttpPost]
public ActionResult Index([Bind(Exclude="Countries")]IndexViewModel model)
{
// do some magic with model.conditions and return another view here
}
Hi~
You don't need to combine the whole SelectListItem to ViewModel, actually your ViewModel just only have a field to store user's choise, integer or string, then use DropDownListFor like:
<%: Html.DropDownListFor(item.WeaponID, MyApplication.Models.DropDownList.GetDropDownList() )%>
please see my post in my blogspot, I use a very simple example to explain:
http://maidot.blogspot.com/2011/04/aspnet-mvc-viewdropdownlistfor.html
let me know if you have any problems : )
I am having this issue where I have a form in a partialview but when validating it jumps out of the parent view and appears on its own (having the submit action url).
Also I tried with Render.Action, where validation does not fire at all.
I am using ASP.NET MVC 2 RC and need the server-side validation to work, using the built in validation. Anyone got any suggestions?
Thanks
Code Partial View:
<%=Html.ValidationSummary() %>
<% using (Html.BeginForm("Edit", "Category", FormMethod.Post))
{%>
<fieldset>
<legend>Edit category</legend>
<p>
<label for="CategoryName">Category name:</label>
<%= Html.TextBox("CategoryName", Model.CategoryName)%>
<%= Html.ValidationMessage("CategoryName", "*")%>
</p>
<p class="submit">
<input type="submit" value="Edit" />
<%=Html.AntiForgeryToken()%>
<%= Html.Hidden("CatId", Model.Id)%>
</p>
</fieldset>
<% }
Model Property:
[Required(ErrorMessage="Required")]
public string CategoryName { get; set; }
Edit Action:
[ValidateAntiForgeryToken]
[HttpPost()]
public ActionResult Edit(int catId, CategoryPageViewModel categoryModel)
{
if (ModelState.IsValid)
{
//TODO
}
return View("list", categoryModel);
}
Does your main view also display category? You're passing "list" a categoryModel. Is this enough for the main view as well as the partial view.