When my Model.State is NOT valid I want to return the view WITH the checked checkboxes.
How would you change my code? Is it possible at all with my approach?
VIEW
#model ListTest.Models.PeopleListViewModel
#{
var hasMoreThanOnePerson = #Model.People.Count > 1;
}
#Html.BeginForm("Save", "Home")
{
#Html.ValidationSummary(false)
<table>
#foreach (var item in Model.People)
{
<tr>
#if (hasMoreThanOnePerson)
{
<td>
<input type="checkbox" name="SelectedIds" value="#item.PersonId" />
</td>
}
else
{
#Html.Hidden("SelectedIds", item.PersonId)
}
<td>
<input type="text" value="#item.Name" />
</td>
</tr>
}
</table>
<input type="submit" value="Save" />
}
VIEWMODEL
public class PeopleListViewModel
{
public PeopleListViewModel()
{
SelectedIds = new int[] { };
}
[MinLength(1, ErrorMessage = "Minimum one person must be selected!")]
public int[] SelectedIds { get; set; }
public List<Person> People { get; set; }
}
CONTROLLER
public ActionResult Index()
{
var people = new List<Person> {
new Person { Name = "Horst", PersonId = 10 },
new Person { Name = "Michael", PersonId = 20}
};
return View(new PeopleListViewModel { People = people });
}
[HttpPost]
public ActionResult Save(PeopleListViewModel viewModel)
{
if (ModelState.IsValid)
{
}
viewModel.People = new List<Person> { new Person { Name = "Horst", PersonId = 10 }, new Person { Name = "bernarnd", PersonId = 20 } };
return View("Index", viewModel);
}
Few things to change
Firstly, change your People model to include an IsSelected property, we want to do away with your SelectedIds method
Secondly, in order to post the data from the client, we need to rewrite your foreach to be a for so the fields are indexed correctly, we'll also add some extra HiddenFors for the properties that you want to keep (because we're no longer re-populating your model when validation fails), your table will be:
<table>
#for (int i = 0; i < Model.People.Count; i++)
{
<tr>
#Html.HiddenFor(m => m.People[i].PersonID)
#Html.HiddenFor(m => m.People[i].Name)
#if (hasMoreThanOnePerson)
{
<td>
#Html.CheckBoxFor(m => m.People[i].IsSelected)
</td>
}
else
{
#Html.HiddenFor(m => m.People[i].IsSelected)
}
<td>
<input type="text" value="#Model.People[i].Name" />
</td>
</tr>
}
</table>
Finally, we don't reassign your People list in your action method if validation fails just return the model that was passed in. If you want to get the selected people, use the code I've added below. Also, because we don't have the SelectedIds anymore we can perform our own validation:
[HttpPost]
public ActionResult Save(PeopleListViewModel viewModel)
{
List<People> selected = viewModel.People
.Where(p => p.IsSelected)
.ToList();
if (selected.Any())
{
//it's valid
List<int> selectedIds = selected
.Select(s => s.PersonID)
.ToList();
}
return View("Index", viewModel);
}
Related
in my journey of learning ASP.NET MVC I encounterd another difficulty:
I'm trying to POST a form with 3 checkboxes, the checkboxes are looped onto the form according to a bound PresentationModel.
I don't know what to fill in at the "asp-for" tag-helpers for the checkboxes in the view so they pass a boolean to the "Create()" ActionResult in the controller and to show the values in the "Overview" View.
Currently it passes NULL for al of them, the other aproaches I tried always resulted in an "InvalidCastException" as it has to be a boolean not an "int[]".
PresentationModel (PMRegistration.cs)
public class PMRegistration
{
public List<Device> Devices { get; set; }
}
View (Index.cshtml)
#model Week3_oef2_ITPro.PresentationModel.PMRegistration
<form asp-controller="Register" asp-action="Create" method="post">
<table>
<tr>
<td>Are you wearing any dangerous accessoires</td>
</tr>
#foreach (var item in Model.Devices)
{
<tr>
<td>#item.Name</td>
<td class="form-group">
<input type="checkbox" asp-for="#item.CheckState" value="#item.Id" class="form-control" />
</td>
</tr>
}
<tr>
<td>
<input type="submit" class="btn btn-default" />
</td>
</tr>
</table>
</form>
Model (Device.cs)
public class Device
{
public int Id { get; set; }
public string Name { get; set; }
public bool CheckState { get; set; }
}
Model (Data.cs, the Device objects get initialized here)
private static List<Device> devices = new List<Device>();
static Data()
{
devices.Add(new Device() { Id = 1, Name = "Laptop" });
devices.Add(new Device() { Id = 2, Name = "Tablet" });
devices.Add(new Device() { Id = 3, Name = "Apple Watch" });
}
public static List<Device> GetDevices()
{
return devices;
}
Controller (RegisterController.cs)
public class RegisterController : Controller
{
// GET: /<controller>/
[HttpGet]
public IActionResult Index()
{
PMRegistration pm = new PMRegistration();
pm.Devices = Data.GetDevices();
return View(pm);
}
public ActionResult Create(PMRegistration pm)
{
if (ModelState.IsValid)
{
return View("Overview", pm);
}
else
{
return RedirectToAction("Index");
}
}
}
------------ SOLVED -------------
With HTML-helpers:
#for (int i = 0; i < Model.Devices.Count; i++)
{
<tr>
<td>
#Model.Devices[i].Name
</td>
<td>
#Html.CheckBoxFor(m => Model.Devices[i].CheckState)
#Html.HiddenFor(m => Model.Devices[i].Id)
#Html.HiddenFor(m => Model.Devices[i].Name)
</td>
</tr>
}
public class SaleItem
{
public int Id { get; set; }
public string Name { get; set; }
}
public class SalesDBContext
{
public static List<SaleItem> GetItems()
{
var items = new List<SaleItem>(){
new SaleItem{Id=1,Name="Soap"},
new SaleItem{Id=2,Name="Milk Power"},
new SaleItem{Id=3,Name="Toothpaste"},
new SaleItem{Id=4,Name="Ice Cream"}
};
return items.ToList();
}
}
public class SalesViewModel
{
public string Item { get; set; }
public List<SaleItem> itemlist { get; set; }
}
I have above SalesViewModel class and SalesDBContext for dummy data generation. I want to add items to a list selected from a dropdown. For that I have created the following view:
#model MVCDropdown.Models.SalesViewModel
#using MVCDropdown.Models
<form method="post">
<p>
#Html.DropDownListFor(model => model.Item, new SelectList(ViewBag.Items, "Id", "Name"), "--select--")
<input type="submit" value="Add" />
</p>
<p>
#if (Model.itemlist != null)
{
<table>
#foreach (var s in Model.itemlist)
{
<tr>
<td>#s.Name</td>
</tr>
}
</table>
}
</p>
</form>
The Controller
[HttpGet]
public ActionResult Index()
{
SalesViewModel model = new SalesViewModel
{
Item = "",
itemlist = new List<SaleItem>()
};
PopDrodown();
return View(model);
}
[HttpPost]
public ActionResult Index(SalesViewModel vm)
{
var t = SalesDBContext.GetItems().Where(x => x.Id == Convert.ToInt32(vm.Item)).FirstOrDefault();
vm.itemlist.Add(t);
PopDrodown();
return View(vm);
}
private void PopDrodown()
{
ViewBag.Items = SalesDBContext.GetItems();
}
Items added to the list should be displayed in a table under the dropdown. However, when I post post back a selected item from the dropdown by pressing add, it returns a null itemlist to the controller, and previously added items are not there. How can I avoid this problem?
You only have a single input element inside your HTML form: that's the dropdown. So the only value that is sent to your controller action when the form is submitted is the Item property. If you want to send the collection you could use hidden fields:
<table>
#for (var i = 0; i < Model.itemlist.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(x => x.itemlist[i].Id)
#Html.HiddenFor(x => x.itemlist[i].Name)
#Html.DisplayFor(x => x.itemlist[i].Name)
</td>
</tr>
}
</table>
Obviously if the user is not supposed to edit the values inside the HTML form, then a much better approach is to have a POST view model that will contain only the properties that the user can modify and you will retrieve the collection elements from the same place you retrieved them in the GET action.
When my Index view display 2 people and none of them are selected the model validation works correctly due to the MinLengthAttribute in .NET 4.5.
Now comes my custom logic in the ui. When only one person is displayed in the Index view I need no checkbox to check it. The customer can directly press the submit button. I try to manually fill the SelectedIds array see the #else clause.
But this code: Model.SelectedIds = new int[]{ item.PersonId};
Does NOT work, the viewmodel.SelectedIds property on server side action is always {int[0]}
How can I still assign the one person id to the SelectedIds array?
VIEW
#model ListTest.Models.PeopleListViewModel
#{
var hasMoreThanOnePerson = #Model.People.Count > 1;
}
#Html.BeginForm("Save", "Home")
{
#Html.ValidationSummary(false)
<table>
#foreach (var item in Model.People)
{
<tr>
#if (hasMoreThanOnePerson)
{
<td>
<input type="checkbox" name="SelectedIds" value="#item.PersonId" />
</td>
}
else
{
Model.SelectedIds = new int[]{ item.PersonId};
}
<td>
<input type="text" value="#item.Name" />
</td>
</tr>
}
</table>
<input type="submit" value="Save" />
}
VIEWMODEL
public class PeopleListViewModel
{
public PeopleListViewModel()
{
SelectedIds = new int[] { };
}
[MinLength(1, ErrorMessage = "Minimum one person must be selected!")]
public int[] SelectedIds { get; set; }
public List<Person> People { get; set; }
}
CONTROLLER
public ActionResult Index()
{
var people = new List<Person> {
new Person { Name = "Horst", PersonId = 10 },
new Person { Name = "Michael", PersonId = 20}
};
return View(new PeopleListViewModel { People = people });
}
[HttpPost]
public ActionResult Save(PeopleListViewModel viewModel)
{
if (ModelState.IsValid)
{
}
viewModel.People = new List<Person> { new Person { Name = "Horst", PersonId = 10 }, new Person { Name = "bernarnd", PersonId = 20 } };
return View("Index", viewModel);
}
You might consider rendering a hidden field in this case to ensure that this value will be sent back to your server:
#if (hasMoreThanOnePerson)
{
<td>
<input type="checkbox" name="SelectedIds" value="#item.PersonId" />
</td>
}
else
{
#Html.Hidden("SelectedIds", item.PersonId)
}
But obviously a much better approach is to handle this on the server - which means that if there's no value assigned in the view model you will simply fetch it back from the backend because the user didn't have the chance to modify this value in the UI since there was no corresponding input element he could manipulate.
I have controller for address I used it to enter multiple addresses but I want to create dropdosnlist to select the person and enter his addresses
I create this helper class in my model folder to create select item
public class PersonsSelectItems
{
public int SelectedId { get; set; }
public List<Person> Persons { get; set; }
}
I use AddressController to send the selectitem to it view
public class AddressController : Controller
{
private readonly CustomersDBEntities context = new CustomersDBEntities();
private PersonsSelectItems personsSelectItems= new PersonsSelectItems();
///get list of persones
///
public List<Person> GetPersonsList()
{
return (from c in personsSelectItems.Persons
select c).ToList();
}
//
// GET: /Address/
public ActionResult Index()
{
//var model = GetPersonsList(); //var model = GetPersonsList().Select(x => new SelectListItem
//{
// Value = x.PersonID.ToString(),
// Text = x.FirstName,
// Selected = true | false
//});
///var model = new PersonsSelectItems { Persons = GetPersonsList() };
var model = GetPersonsList();
return View(model);
}
//
// GET: /Address/Welcome/
public string Welcome()
{
return "This is the Welcome action method...";
}
[HttpPost]
public ActionResult Create(Address address)
{
//Loop through the request.forms
var Addesslist = new List<Address>();
for (int i = 1; i <= Request.Form.Count; i++)
{
var street = Request.Form["street_0" + i + ""];
var city = Request.Form["city_0" + i + ""];
var postalCode = Request.Form["postalCode_0" + i + ""];
var province = Request.Form["province_0" + i + ""];
var personID = 1;
if (street != null && city != null && postalCode != null && province != null)
{
try
{
context.Addresses.Add(new Address
{
Street = street,
City = city,
Province = province,
PostalCode = postalCode,
PersonID = personID
});
context.SaveChanges();
}
catch (Exception exc)
{
}
}
else
{
break;
}
}
return RedirectToAction("Index");
}
}
I get this expsetion
Value cannot be null. Parameter name: source
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more
information about the error and where it originated in the code.
Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: source
Adress view
#model MVC.Models.Address
Tuple<Person,Order>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#using (Html.BeginForm("Create", "Address", FormMethod.Post))
{
#Html.DropDownListFor(x => x.Person, new SelectList(Model.Person, "PersonId", "FirstName"))
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="table-responsive">
<table id="address_table" class="table">
<thead>
<tr>
<th>Street</th>
<th>City</th>
<th>Province</th>
<th>PostalCode</th>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input id="Text1" type="text" name="street_01" maxlength="255" required class="street" /></td>
<td>
<input id="Text2" type="text" name="city_01" maxlength="255" required class="city" /></td>
<td>
<input id="Text3" type="text" name="province_01" maxlength="255" required class="province" /></td>
<td>
<input id="Text4" type="text" name="postalCode_01" maxlength="7" required class="postalCode" /></td>
<td> </td>
</tr>
</tbody>
</table>
</div>
<input type="button" value="Add Row" id="add_AdressRow" class="btn btn-lg btn-success btn-block" />
<p>
<input type="submit" value="Create" />
</p>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
just want to ask how can I bind dropDwonList using list from GetPersonsList() function
from AdressController and bind I now their are away to do it but I could not find it ?
Your problem is that you're trying to use some LINQ over a null list.
This bad boy here:
public List<Person> Persons { get; set; }
Is null. You can add a constructor to your type to initialize it:
public class PersonsSelectItems
{
public int SelectedId { get; set; }
public List<Person> Persons { get; set; }
public PersonsSelectItems() {
Persons = new List<Person>();
}
}
..and that will stop your current error.
I have to point out a couple of things though. Firstly, the naming Persons is strange. Make it an English plural of People.
Secondly, you don't actually have to use LINQ here. Your GetPersonList method can simply be:
public List<Person> GetPersonsList()
{
return personsSelectItems.Persons;
}
Even then.. you have access to that collection already. So your model assignment can be:
var model = _personsSelectItems.Persons;
I have a model women which is part of my db context and a view Model womenEditmodel which conatins a list of women items. I am using a partialview to loop through this list and display an editable Grid or List in my view. These are my models:
public class Women
{
public string ID { get; set; }
public string FirstName {get; set;}
public string LastName {get; set;}
}
public class WomenEditModel
{
public List<Women> WomenList { get; set; }
}
My view has this loop for injecting into my view rows for the women records
#foreach (Women women in Model.Womens)
{
Html.RenderPartial("WomenEditor", women);
}
which i display using a table. So now users can edit the list and post or save changes.
my partialview looks like:
#model XXX.Models.Women
#using (Html.BeginCollectionItem("Women")) {
<td>
#Html.HiddenFor(model => model.ID)
</td>
<td>
#Html.TextBoxFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName)
</td>
<td>
#Html.TextBoxFor(model => model.LastName)
#Html.ValidationMessageFor(model => model.LastName)
</td>
}
My http post action method looks like this
[HttpPost]
public ActionResult PostWomen(WomenEditModel model)
{
/*I need to iterate through the returned list and save all
changes to the db.*/
return RedirectToAction("Index");
}
How do i loop through the model WomenEditModel recieived at the post action method and save changes to the women list back to db?
Thanks in advance!!
I just got back to my machine. Here is how you can acheive it, if you haven't already.
My Action which renders the list
public ActionResult Index()
{
List<Women> womens = new List<Women>
{
new Women
{
Id=1,
FirstName = "Women1",
LastName = "Lastname1"
},
new Women
{
Id=2,
FirstName = "Women2",
LastName = "Lastname2"
}
};
WomenList womenList=new WomenList();
womenList.Womens = womens;
return View(womenList);
}
The action where the list is posted.
public ActionResult SaveWomens(List<Women> womenList)
{
System.Diagnostics.Debugger.Break();
//Your save logic goes here
return View("");
}
Partial View (Dont know whether it is required)
#model List<MvcApplication1.Models.Women>
<table>
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.TextBoxFor(m => m[i].Id)
</td>
<td>#Html.TextBoxFor(m => m[i].FirstName)
</td>
<td>#Html.TextBoxFor(m => m[i].LastName)
</td>
</tr>
}
</table>
And here is the view
#model MvcApplication1.Models.WomenList
#{
ViewBag.Title = "Home Page";
}
#section featured {
}
#using (Html.BeginForm("SaveWomens", "Home", FormMethod.Post))
{
#Html.Partial("_Women", Model.Womens)
<input type="submit" value="save" />
}
You can achieve it as follows
foreach (var item in womenList)
{
var obj = new Womens();
//Assign values to obj for eg: obj.prop = item.prop
dataContext.Womens.AddObject(obj);
}
dataContext.SaveChanges();