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

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

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" })

#Html.DisplayNameFor() respects [Display] attribute but #Html.DisplayFor() ignores [DisplayFormat] attribute

I have this view model:
public class ConceptViewModel
{
public int Id { get; set; }
[Required(ErrorMessageResourceName = "Message_Required", ErrorMessageResourceType = typeof(Resources.Web.ValidationsViewModels))]
[Display(Name = "ConceptViewModel_Name", ResourceType = typeof(Resources.Web.Contracts))]
public string Concept { get; set; }
[Required(ErrorMessageResourceName = "Message_Required", ErrorMessageResourceType = typeof(Resources.Web.ValidationsViewModels))]
[Display(Name = "ConceptViewModel_TotalUnits", ResourceType = typeof(Resources.Web.Contracts))]
[DisplayFormat(DataFormatString = "{0:###,###}")] // Ignored by #Html.DisplayFor()
public decimal TotalUnits { get; set; }
}
That view model is inside a collection property of another view model that happens to be the one passed to the view:
public class ContratConceptsViewModel : BaseClass
{
public int ContratId { get; set; }
// This collection
public List<ConceptViewModel> Concepts { get; set; }
public ContratConceptsViewModel()
{
Concepts = new List<ConceptViewModel>();
}
}
And then, inside the view, I have this code:
#model ContratConceptsViewModel
#foreach (var concept in Model.Concepts)
{
<thead>
<tr>
<td> #Html.DisplayNameFor(model => concept.Concept) </td> #* Works as expected *#
<td> #Html.DisplayNameFor(model => concept.TotalUnits) </td> #* Works as expected *#
</tr>
</thead>
<tbody>
<tr>
<td> #Html.DisplayFor(model => concept.Concept) </td>
<td> #Html.DisplayFor(model => concept.TotalUnits) </td> #* Doesn't show the correct format *#
</tr>
</tbody>
}
When I execute this code, the [Display] attribute is taken into account, but the [DisplayFormat] attribute is totally ignored. What am I doing wrong?
NOTE: I have searched for a question like this one, and despite there are many others with a similar title, they are different from this one.
Ok, it was all my fault. One of my coworkers created a template for decimal inside the folder Views\Shared\DisplayTemplates\Decimal.cshtml. That's why #Html.DisplayFor() was ignoring the attribute, it was because it was respecting the template created by my coworker.
You should amend you expressions inside DisplayFor and DisplayNameFor.
It must be #Html.DisplayFor(model => model.TotalUnits), not #Html.DisplayFor(model => concept.TotalUnits).
UPDATE
Because you're displaying items from the list #Html.DisplayFor will not work properly: it's supposed to work with single model. I would suggest to extract your template for each item into separate file(it's called display template in MVC):
Your display template(it must be placed in DisplayTemplates folder):
#model ConceptViewModel
<thead>
<tr>
<td> #Html.DisplayNameFor(model => model.Concept) </td>
<td> #Html.DisplayNameFor(model => model.TotalUnits) </td>
</tr>
</thead>
<tbody>
<tr>
<td> #Html.DisplayFor(model => model.Concept) </td>
<td> #Html.DisplayFor(model => model.TotalUnits) </td>
</tr>
</tbody>
Your main page:
#model ContratConceptsViewModel
#foreach (var concept in Model.Concepts)
{
#Html.DisplayFor(model => concept, "name of your display template, optional if you follow conventions")
}
Here is the link about display templates.

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

Posted model is empty

I have a ViewModel as following
public class MetalStockAddVM
{
public MetalStock MetalStock { get; set; }
public MetalStockAddVM()
{
this.MetalStock=new MetalStock();
}
}
Here is My controller
public class MetalStockController : Controller
{
private readonly IMetalStockRepository iMetalStockRepository;
public MetalStockController(IMetalStockRepository iMetalStockRepository)
{
this.iMetalStockRepository = iMetalStockRepository;
}
// GET: MetalStock
[HttpGet]
public ActionResult AddMetalStock()
{
MetalStockAddVM addVm=new MetalStockAddVM();
return View(addVm);
}
[HttpPost]
public ActionResult AddMetalStock([Bind(Include = "MetalStock")]MetalStockAddVM metalStock)
{
MetalStockDto metalStockDto = new MetalStockDto();
metalStockDto = Mapper.Map<MetalStock, MetalStockDto>(metalStock.MetalStock);
iMetalStockRepository.Insert(metalStockDto);
return RedirectToAction("Index","Home") ;
}
}
Here is my view
#model LearningSpike.Models.ViewModels.MetalStockAddVM
#using (Html.BeginForm("AddMetalStock","MetalStock",FormMethod.Post))
{
<table>
<tr>
<th>
#Html.LabelFor(m => m.MetalStock.MetalId)
</th>
<td>
#Html.EditorFor(m => m.MetalStock.MetalId)
</td>
</tr>
<tr>
<th>
#Html.LabelFor(m => m.MetalStock.GlobalMaterialId)
</th>
<td>
#Html.EditorFor(m => m.MetalStock.GlobalMaterialId)
</td>
</tr>
<tr>
<th>
#Html.LabelFor(m => m.MetalStock.Length)
</th>
<td>
#Html.EditorFor(m => m.MetalStock.Length)
</td>
</tr>
<tr>
<th>
#Html.LabelFor(m => m.MetalStock.ColourCode)
</th>
<td>
#Html.EditorFor(m => m.MetalStock.ColourCode)
</td>
</tr>
<tr>
<th>
#Html.LabelFor(m => m.MetalStock.QuantityInStock)
</th>
<td>
#Html.EditorFor(m => m.MetalStock.QuantityInStock)
</td>
</tr>
</table>
<input type="submit" value="Create"/>
}
When I post the model that is passed to the controller action method is empty .
Please tell me what I'm doing wrong? I am creating an architectural spike for the first time so I can learn a lot.Thus its my first time implementing all heavy stuff (DI , DTO's , REpositories etc.) . I am still struggling with putting things in right places.
Thanks!
Cheers!
First remove [Bind(Include = "MetalStock")] and second rename metalStock to some something like metalStockVm and see does it works or not.
[HttpPost]
public ActionResult AddMetalStock(MetalStockAddVM metalStockVm)
Please try the below one
change your model code like below
public class MetalStockAddVM
{
public MetalStock MetalStock { get; set; }
}
And then change you Action method code for HTTPPOST in AddMetalStock as below
[HttpPost]
public ActionResult AddMetalStock(MetalStockAddVM metalStock)
{
MetalStockDto metalStockDto = new MetalStockDto();
metalStockDto = Mapper.Map<MetalStock, MetalStockDto>(metalStock.MetalStock);
iMetalStockRepository.Insert(metalStockDto);
return RedirectToAction("Index","Home") ;
}

list with deleted items does not bind correctly

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.

Resources