list with deleted items does not bind correctly - asp.net

I have a view in which the user can edit all fields for a list of items and two buttons "Add" and "Delete" which should add a new item or delete all selected items, respectively.
Although the add functionality works properly, the delete option removes the correct number of items, but removes items from the end of the list regardless of which were selected.
Strangely, the correct items seem to be deleted in the controller and passed to the view, but the view renders with the last few items deleted instead of the selected ones. Here is the code:
Model:
public class TestClass
{
public bool IsSelected { get; set; }
public string Prop1 { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
}
View:
#model List<TestMvcApp.Models.TestClass>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
#using (Html.BeginForm()) {
<table>
<tr>
<th>
#Html.DisplayNameFor(model => model[0].IsSelected)
</th>
<th>
#Html.DisplayNameFor(model => model[0].Prop1)
</th>
<th>
#Html.DisplayNameFor(model => model[0].Prop2)
</th>
<th>
#Html.DisplayNameFor(model => model[0].Prop3)
</th>
</tr>
#for (int i = 0; i < Model.Count(); ++i )
{
<tr>
<td>
#Html.EditorFor(model => model[i].IsSelected)
</td>
<td>
#Html.EditorFor(model => model[i].Prop1)
</td>
<td>
#Html.EditorFor(model => model[i].Prop2)
</td>
<td>
#Html.EditorFor(model => model[i].Prop3)
</td>
</tr>
}
</table>
<input type="submit" name="buttonPressed" value="Add" />
<input type="submit" name="buttonPressed" value="Remove" />
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
List<TestClass> testList = new List<TestClass>();
testList.Add(new TestClass());
return View(testList);
}
[HttpPost]
public ActionResult Index(List<TestClass> list, string buttonPressed)
{
switch (buttonPressed)
{
case "Add":
return Add(list);
case "Remove":
return Remove(list);
default:
return View(list);
}
}
public ActionResult Add(List<TestClass> list)
{
list.Add(new TestClass());
return View("Index", list);
}
public ActionResult Remove(List<TestClass> list)
{
for( int i = list.Count() - 1 ; i >= 0 ; --i)
{
if (list[i].IsSelected)
list.RemoveAt(i);
}
if (list.Count <= 0)
list.Add(new TestClass());
return View("Index", list);
}
}
When I break at the return statement, the list contains the correct items, but the view contains only the first items (e.g. if the list has 5 items and I remove items 2 and 3, the controller will show a list with items 1,4,5 but the view will render a list with items 1,2,3).
Any ideas why this does not work as expected?

I think ModelState is causing you these headaches. Try adding ModelState.Clear() in your post action before you return your View.

Related

Checkbox in ASP.NET MVC without using HTML helper method

I've written this code in View. And the picture is the output of it in Chrome Inspect.
#Html.CheckBox("KYCComplete", new { #name = "KYC", #id = "KYC" })
<label class="form-check-label" for="KYC">
KYC Complete ?
<button id="Submit">KYC Complete ?</button>
</label>
In my controller I'm using this HttpPost to use checkbox an filter:
[HttpPost]
public ActionResult Index(bool KYCComplete)
{
if (KYCComplete)
{
List<BankAccount> b= db.BankAccounts.Where(p => p.KYCComplete == true).OrderBy(b => b.City).ToList();
return View(b);
}
return RedirectToAction("Index");
}
Everything works fine, up to this point. Only name property is not overridden.
Well, I want to change the name property of the checkbox to be "KYC" from "KYCComplete".
So, firstly I look for Is there any ways to override HTML helpers. I found in several websites it's not possible to override those.
Now I tried with writing simple HTML checkbox and I'm getting an error.
Server Error in '/' Application.
The parameters dictionary contains a null entry for parameter
'KYCComplete' of non-nullable type 'System.Boolean' for method
'System.Web.Mvc.ActionResult Index(Boolean)' in
'BankAccountsMgmt.Controllers.BankAccountsController'. An optional
parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters
So how can I change the name property of this checkbox to "KYC" and bind its input, to filter the desired result.
Describing the question In a Nutshell
As, you have seen the output of the view of this checkbox has name property "KYCComplete".
I've requirement to change to "KYC", and HttpPost should work along with it, without effecting domain model.
Incase extra info. is Required
model:
namespace BankAccountsMgmt.Models
{
public class BankAccount
{
...
[Display(Name = "KYC Complete?")]
public bool KYCComplete { get; set; }
...
}
}
controller:
using BankAccountsMgmt.Data;
using BankAccountsMgmt.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace BankAccountsMgmt.Controllers
{
[CustomFilters.CustomExceptionFilter]
public class BankAccountsController : Controller
{
BankAccountDBContext db = new BankAccountDBContext();
// GET
public ActionResult Index()
{
//Implement your code
List<BankAccount> bank = db.BankAccounts.OrderBy(b=>b.City).ToList();
return View(bank);
}
//Implement other action methods
[HttpPost]
public ActionResult Index(bool KYCComplete)
{
if (KYCComplete)
{
List<BankAccount> AccKYC = db.BankAccounts.Where(p => p.KYCComplete == true).OrderBy(b => b.City).ToList();
return View(AccKYC);
}
return RedirectToAction("Index");
}
public ActionResult AddBankAccount()
{
return View();
}
[HttpPost]
public ActionResult AddBankAccount(BankAccount bankAccount)
{
if (ModelState.IsValid)
{
bankAccount.CalculateInterest();
db.BankAccounts.Add(bankAccount);
db.SaveChanges();
ViewBag.Message = "Bank Account added successfully!";
return View("Details", bankAccount);
}
return View(bankAccount);
}
}
}
full view:
#model List<BankAccountsMgmt.Models.BankAccount>
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Bank Accounts List</h2>
#using (Html.BeginForm("Index", "BankAccounts"))
{
<div class="form-check col-md-offset-8" align="center">
#Html.CheckBox("KYCComplete", new { #name = "KYC", #id = "KYC" })
<label class="form-check-label" for="KYC">
KYC Complete ?
<button id="Submit">KYC Complete ?</button>
</label>
</div>
<table class="table">
<tr>
<th>
Account Holder Name
</th>
<th>
PAN Number
</th>
<th>
City
</th>
<th>
Gender
</th>
<th>
Amount
</th>
<th>
Interest Upto 30 Aug
</th>
<th>
Opening Date
</th>
<th>
KYC Complete?
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.AccountHolderName)
</td>
<td>
#Html.DisplayFor(modelItem => item.PAN)
</td>
<td>
#Html.DisplayFor(modelItem => item.City)
</td>
<td>
#Html.DisplayFor(modelItem => item.Gender)
</td>
<td>
#Html.DisplayFor(modelItem => item.Amount)
</td>
<td>
#Html.DisplayFor(modelItem => item.Interest)
</td>
<td>
#Html.DisplayFor(modelItem => item.OpeningDate)
</td>
<td>
#Html.DisplayFor(modelItem => item.KYCComplete)
</td>
</tr>
}
</table>
<span id="total" class="form-check col-md-offset-6" align="center"><b>Interest Total = </b>#Model.Sum(model => model.Interest).ToString("#.##") </span>
}
You could try Html Checkbox instead of using html helper method:
Example:
<input type="checkbox" id="KYC" name="KYC">
<label for="KYC"> KYC Complete</label>
The name of your form control must match the name of the parameter to your action method for model binding to work.
If you change the name of the checkbox to "KYC", but leave the action parameter as "KYCComplete", you will get the exception mentioned in your question.
Change the name in both places.
[HttpPost]
public ActionResult Index(bool KYC)
{
if (KYC)
{
List<BankAccount> b= db.BankAccounts.Where(p => p.KYCComplete == true).OrderBy(b => b.City).ToList();
return View(b);
}
return RedirectToAction(nameof(Index));
}
#Html.CheckBox("KYC", new { #id = "KYC" })

MVC Core posting a dynamic collection

I'm new to MVC and feel like I'm missing something conceptually. I'm trying to create a page where I can submit multiple records at once, dynamically adding and removing records via ajax
I've been trying to follow examples like Steven Sanderson and haacked but whenever I submit the page the collection contains 0 records.
Looking at Fiddler I can see both rows being submitted yet something goes missing.
I've seen references to editor templates and html helpers to make this prettier but these have mostly gone over my head and most of the examples I've found are in old versions of MVC as well.
Model
public class Projection
{
public int ID { get; set; }
[Display(Name = "Project")]
public int ProjectId{ get; set; }
[Display(Name = "Employee")]
public int EmployeeId { get; set; }
public decimal Hours { get; set; }
[DisplayFormat(DataFormatString = "{0:MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime Month { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Project Project { get; set; }
public virtual Employee Employee { get; set; }
}
Controller
// GET: Projections/CreateMulti
[HttpGet]
public IActionResult CreateMulti()
{
ViewBag.Index = 0;
ViewData["EmployeeId"] = new SelectList(_context.Employees.Where(e => e.Active).OrderBy(e => e.FullName), "ID", "FullName");
PopulateClientsDropDownList();
PopulateProjectsDropDownList();
return View(new Projection { Month = DateTime.Today.AddMonths(1).AddDays(1 - DateTime.Today.Day) });
}
[HttpGet]
public IActionResult GetNewRow(int index)
{
ViewBag.Index = index;
ViewData["EmployeeId"] = new SelectList(_context.Employees.Where(e => e.Active).OrderBy(e => e.FullName), "ID", "FullName");
PopulateClientsDropDownList();
PopulateProjectsDropDownList();
return PartialView("CreateMultiPartial", new Projection { Month = DateTime.Today.AddMonths(1).AddDays(1 - DateTime.Today.Day) });
}
// POST: Projections/CreateMulti
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateMulti(ICollection<Projection> ProjectionList)
{
foreach (Projection item in ProjectionList)
{
//do something here
}
return View(ProjectionList);
}
View
#model Test.Models.Projection
#{int Index = 1;
string newRowUrl = #Url.Action("GetNewRow", "Projections", new { index = #Index });}
<form asp-action="CreateMulti">
<table class="table" id="myTable">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Project.Client)
</th>
<th>
#Html.DisplayNameFor(model => model.Project)
</th>
<th>
#Html.DisplayNameFor(model => model.Employee)
</th>
<th>
#Html.DisplayNameFor(model => model.Month)
</th>
<th>
#Html.DisplayNameFor(model => model.Hours)
</th>
<th></th>
</tr>
</thead>
<tbody>
#{Html.RenderPartial("CreateMultiPartial", Model);}
</tbody>
</table>
<div>Add Another Row</div>
<div>
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$("#addNew").click(function () {
event.preventDefault();
$.ajax({
url: $("#addNew").attr("href"),
cache: false,
success: function (html) {
$('#myTable > tbody:last-child').append(html);
indexIterate();
}
});
});
var currentIndex = 1;
function indexIterate() {
var newHref = $("#addNew").attr("href");
var newerHref = newHref.replace(/(?:index=)[0-9]+/i, "index=" + ++currentIndex);
$("#addNew").attr("href", newerHref);
};
});
</script>
}
Fiddler
Projections.Index=0&ClientID=7&Projections%5B0%5D.ProjectId=40&Projections%5B0%5D.EmployeeId=3&Projections%5B0%5D.Month=10%2F2017&Projections%5B0%5D.Hours=1&Projections.Index=1&ClientID=15&Projections%5B1%5D.ProjectId=19&Projections%5B1%5D.EmployeeId=32&Projections%5B1%5D.Month=10%2F2017&Projections%5B1%5D.Hours=1&__RequestVerificationToken=AAA

Radio button list using editor templates for merging contacts

I am trying to display a form for merging two contacts in ASP.net MVC 5.
The form should look like this where each row holds a radio button group consisting of 2 options:
The form shows up just fine, and the radio groups work (I can select each group). However, when the model is posted back to the server, the Values list is empty (not preserved). I would like to get both the selected id but of course also the actual text value back in the controller. I prefer to do this using Editor Templates and if possible without for loops.
The current results (Values = null):
EDIT to respond to comments: I would prefer not to re-fetch the values in the controller again, because it results in a call to a web service. I have tried some variants of HiddenFor without results.
My models look like this:
public class Contact
{
public List<ContactRow> Rows { get; set; }
public Contact()
{
this.Rows = new List<ContactRow>();
this.Rows.Add(new ContactRow("First Name", "Homer", "Homie"));
this.Rows.Add(new ContactRow("Last Name", "Simpson", "Simson"));
this.Rows.Add(new ContactRow("Email", "mail1", "mail2"));
this.Rows.Add(new ContactRow("Company Phone", "Phone1", "Phone2"));
this.Rows.Add(new ContactRow("Mobile Phone", "Mobile1", "Mobile2"));
}
}
public class ContactRow
{
public int Selection { get; set; }
public string Label { get; set; }
public List<ValueSet> Values { get; set; }
public ContactRow(string Label, string LeftValue, string RightValue, int Selection = 0)
{
if (LeftValue== null) LeftValue= "";
if (RightValue== null) RightValue= "";
this.Label = Label;
this.Selection = Selection;
this.Values = new List<ValueSet>(2);
this.Values.Add(new ValueSet() { ID = 0, ValueText = LeftValue});
this.Values.Add(new ValueSet() { ID = 1, ValueText = RightValue});
}
public ContactRow() { }
}
public class ValueSet
{
public int ID { set; get; }
public string ValueText { set; get; }
}
The Controller:
public ActionResult Index()
{
Contact model = new Contact();
return View(model);
}
public ActionResult MergeContacts(Contact model)
{
return RedirectToAction("Index");
}
And the views:
Index.cshtml
#model RadioTest.Models.Contact
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm("MergeContacts", "Home", FormMethod.Post, new { encType = "multipart/form-data", id = "contactDetailsForm", name = "contactDetailsForm" }))
{
<div>
<table class="table" width="100%">
<tr>
<th style="text-align:left">Label</th>
#*<th style="text-align:right">R1</th>*#
<th style="text-align:left">
Left Value
</th>
#*<th style="text-align:right">R2</th>*#
<th style="text-align:left">
Right Value
</th>
</tr>
#Html.EditorFor(model => model.Rows)
</table>
<input type="submit" />
</div>
}
Editor Template for ContactRow:
ContactRow.cshtml
#model RadioTest.Models.ContactRow
<tr>
<td style="text-align:left">
#Html.DisplayFor(model => model.Label)
</td>
#foreach (var v in Model.Values)
{
<td style="text-align:left">
#Html.RadioButtonFor(model => model.Selection, v.ID) #v.ValueText
</td>
}
</tr>
#Html.HiddenFor(model => model.Label)
Just change your foreach to for:
#model MVCApp.Controllers.ContactRow
<tr>
<td style="text-align:left">
#Html.DisplayFor(model => model.Label)
</td>
#for (int i = 0; i < Model.Values.Count; i++)
{
<td style="text-align:left">
#Html.RadioButtonFor(model => model.Selection, Model.Values[i].ID) #Model.Values[i].ValueText
#Html.HiddenFor(model => Model.Values[i].ID)
#Html.HiddenFor(model => Model.Values[i].ValueText)
</td>
}
</tr>
#Html.HiddenFor(model => model.Label)

ASP.Net MVC Posted form values not mapping back onto viewmodel in controller

My ViewModel is:
public class ObjectiveVM
{
public string DateSelected { get; set; }
public List<string> DatePeriod { get; set; }
public IList<ObList> obList { get; set; }
public class ObList
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AnalystName { get; set; }
public bool Include { get; set; }
}
}
This is passed to the view, populated as expected - and displays correctly in the view.
My problem is when it is posted back to the controller. My controller code to accept it back is:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Analyst(ObjectiveVM ovm)
ovm.obList is always showing as null:
My View html is:
#model Objectives.ViewModels.ObjectiveVM
#{
ViewBag.Title = "Analyst";
}
<h2>Copy Objectives for Analyst</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Objective</legend>
#Html.DropDownListFor(model => model.DateSelected, new SelectList(Model.DatePeriod))
<table>
<tr>
<th>
#Html.DisplayNameFor(model => model.obList[0].Include)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].AnalystName)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].Title)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].Description)
</th>
</tr>
#foreach (var obList in Model.obList)
{
<tr>
<td>
#Html.HiddenFor(modelItem => obList.ObjectiveId)
#Html.HiddenFor(modelItem => obList.AnalystId)
#Html.HiddenFor(modelItem => obList.Title)
#Html.HiddenFor(modelItem => obList.Description)
#Html.CheckBoxFor(modelItem => obList.Include)
</td>
<td>
#Html.DisplayFor(modelItem => obList.AnalystName)
</td>
<td>
#Html.DisplayFor(modelItem => obList.Title)
</td>
<td>
#Html.DisplayFor(modelItem => obList.Description)
</td>
</tr>
}
</table>
<p>
<input type="submit" value="Copy Selected Objectives" />
</p>
</fieldset>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Looking in Developer Tools at the Posted form values, they appear to be ok:
Can anyone see any reason the posted form values, are not mapping back onto my viewmodel in the Controller HTTP post?
Thank you, Mark
You need to use a for...loop here, not a foreach....loop.
#for (int idx = 0;idx < Model.obList.Count;idx++){
#Html.HiddenFor(_ => Model.obList[idx].ObjectiveId)
// ... etc....
}
Without the indexer (idx), the model binder will not know how to bind the values back to the right collection item.
When working with collections in my views, I typically write out my markup without the use of helpers:
#for (int i = 0;i < Model.obList.Count();i++){
<input type="hidden" name="ObList[#i].ObjectiveId" id="ObList[#i].ObjectiveId" value="#ObList[i].ObjectiveId" />
<input type="hidden" name="ObList[#i].AnalystId" id="ObList[#i].AnalystId" value="#ObList[i].AnalystId" />
...
}
This will conform to the wire format the model binder expects, and will slot your values into your ViewModel: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

MVC and Entity Framework Select List

I have an MVC app that I am trying to put together that requires some select lists and drop down lists.
So, I have the following models....
public class Task
{
public int ID { get; set; }
public string Title { get; set; }
......
public virtual ICollection<Monitor> Monitors { get; set; }
public virtual ICollection<Resource> Resources { get; set; }
}
public class Monitor
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IList<Task> Tasks { get; set; }
}
public class Resource
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
.....
public IList<Task> Tasks { get; set; }
The interesting part is that when I display a list of tasks, among the other properties that display just fine, I need to display a list of 'Monitors' and a list of 'Resources' that are assigned to the task in the Index view shown below.
#model IEnumerable<ResourceManager.Models.Task>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
.....
<th>
#Html.DisplayNameFor(model => model.Title)
</th>
.....
<th>
#Html.DisplayNameFor(model => model.Monitors)
</th>
<th>
#Html.DisplayNameFor(model => model.Resources)
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
.....
<td>
#Html.DisplayFor(modelItem => item.Title)
</td>
.....
<td>
#if (item.Monitors == null || item.Monitors.Count == 0)
{<span>No Monitors Assigned</span>}
else
{ string.Join(", ", item.Monitors.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName))); }
</td>
<td>
#Html.DisplayFor(modelItem => item.Resources)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
#Html.ActionLink("Details", "Details", new { id=item.ID }) |
#Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
And here is the controller....
public ActionResult Index()
{
var tasks = from t in db.Tasks where t.IsActive == true select t;
return View(tasks);
}
I would like for the list of Monitors and the list of Resources to display as a string on the Index, Delete and Details Views i.e. 'Monitor 1, Monitor 2, Monitor 3' and 'Resource 1, Resource 2, Resource 3'.
However on the other views (Create and Edit), I want them to appear as a selectable list.
First Create Select list in your controller,
var monitors = //Your linq query
ViewData["ddlList"] = monitors .Select(x => new SelectListItem {
Text = x.FirstName,
Value = x.Id.ToString()
}).ToList();
And then you can use it in your view as follows,
<%=Html.DropDownList("myList") %>
For the display of Monitors/Resources (and since you want them displayed as a comma-delimited list), you can just use string.Join:
<td>
#string.Join(",", Model.Monitors.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName)))
</td>
To be able to actually use Html.DisplayFor, you'd have to create a custom display template so Razor will actually know how to respond. To do so, in your Views folder, create new folder called "DisplayTemplates", and in that, create a new partial view called "Monitors.cshtml" and "Resources.cshtml", strongly-typed to IEnumerable<Monitor> and IEnumerable<Resource>, respectively. Then inside that file, you'll just add roughly the same code as above:
#model IEnumerable<Monitor>
#string.Join(",", Model.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName)))
Then in your view, you can call:
#Html.DisplayFor(m => m.Monitors, "Monitors")
Unfortunately, in this example, you'd have to feed the template name because the default behavior of DisplayFor for a list, is to render the display template multiple times, once for each member of the list. You could do something like:
# Monitor.cshtml
#model Monitor
#Model.FirstName #Model.LastName,
And then:
#Html.DisplayFor(m => m.Monitors)
But your last item would have a comma at the end.
For editing the lists, all you have to do is pass the select lists to your view. Optimally, you'd do this with a view model, but for simplicity's sake, I'll just use ViewBag here. In your controller action:
ViewBag.MonitorChoices = db.Monitors.Select(m => new SelectListItem
{
Value = m.ID.ToString(),
Text = string.Format("{0} {1}", m.FirstName, m.LastName)
});
Then, in your create/edit view:
#Html.ListBoxFor(m => m.Monitors, ViewBag.MonitorChoices)
Try as follows,
var monitors = //Your linq query
List monitorList = new List();
foreach (var monitor in monitors ){
SelectListItem item = new SelectListItem();
item.Text = monitor.FirstName;
item.Value = monitor.Id.ToString();
monitorList .Add(item);
}
ViewData["ddlList"] = monitorList;

Resources