Asp.net core razor pages [BindProperty] doesnt work on collections - data-binding

Im trying to use [BindProperty] annotation in asp.net core razor pages in order to Bind an Ilist<T> collection of one of my model classes so i can edit some of them at once, but it doesnt work at all, every time in OnPostAsync function the collection is empty, and neither the changes that i made on data nor it default values wont post back to the server, but when its a singel object [BindProperty] works fine and the values post back and can be changed, i also tried wraping a collection (i.e list<T>) in an object but it didnt work either way, so is there any way for doing so or i should lets say send a edit request for every object in that collection and edit them one by one(which cant be done in razor pages easilly and need some ajax calls)??

For binding IList between RazorPage and PageModel, you will need to use Product[i].Name to bind property.
Here are complete steps.
Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
PageModel
public class IndexModel : PageModel
{
private readonly CoreRazor.Data.ApplicationDbContext _context;
public IndexModel(CoreRazor.Data.ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public IList<Data.Product> Product { get; set; }
public async Task OnGetAsync()
{
Product = await _context.Product.ToListAsync();
}
public async Task OnPostAsync()
{
var product = Product;
}
}
View
<form method="post">
<table class="table">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Product[0].Name)
</th>
<th></th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.Product.Count(); i++)
{
<tr>
<td>
<input hidden asp-for="Product[i].Id" class="form-control"/>
<input asp-for="Product[i].Name" class="form-control" />
</td>
<td>
<a asp-page="./Edit" asp-route-id="#Model.Product[i].Id">Edit</a> |
<a asp-page="./Details" asp-route-id="#Model.Product[i].Id">Details</a> |
<a asp-page="./Delete" asp-route-id="#Model.Product[i].Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>

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

Trying to update my users password using AddPasswordAsync but passwords always left as null

So i've got 2 tables one for Editing user roles and one fore editing user accounts the roles table allows me to delete and add roles perfectly fine but i'm having trouble with updating passwords. The passwords seem to be getting deleted but not updated with the new one specified.
<br>
<h3>Roles Table</h3>
<table class="table table-striped">
<thead>
<tr> <th>Id</th><th>User Roles</th> </tr>
</thead>
<tbody>
#foreach (var roles in Model.roles)
{
<tr>
<td> #roles.Id</td>
<td> #roles.Name</td>
<td><a class="btn btn-sm btn-danger order-button float-right" asp-page="/ManageRoles" asp-route-id="Delete" asp-page-handler="Delete">Delete Roles </a></td>
</tr>
}
</tbody>
</table>
<form method="post">
<div class="flex">
<div>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
<div class="input-group">
<input type="text" class="form-control" asp-for="#Model.roleName">
</div>
</div>
</form>
<table class="table table-striped" style="margin-top: 100px;">
<thead>
<tr> <th>Id</th><th>User Account</th> </tr>
</thead>
<tbody>
#foreach (var users in Model.users)
{
<tr>
<td> #users.Id</td>
<td> #users.Email</td>
<td><a class="btn btn-sm btn-danger order-button float-right" asp-page="/ManageRoles" asp-route-id="DeleteUser" asp-page-handler="DeleteUser">Delete User </a></td>
<td>
<form method="post" asp-page-handler="Update" asp-route-id="#users.Id">
<div class="input-group">
<input type="text" class="form-control" asp-for="#Model.NewPassword">
<button type="submit" class="btn btn-default">Update Password </button>
</div>
</form>
</td>
</tr>
}
</tbody>
</table>
So heres my Page model class below
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<AppUser> _userManager;
public List <IdentityRole> roles { get; set; }
[BindProperty(SupportsGet = true)]
public string Id { get; set; }
[BindProperty]
public string roleName { get; set; }
public List <AppUser> users { get; set; }
[BindProperty]
public string userId { get; set; }
[BindProperty]
public string NewPassword { get; set; }
public AdminDashboardModel(RoleManager<IdentityRole> roleManager, UserManager<AppUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public void OnGet()
{
roles = _roleManager.Roles.ToList();
users = _userManager.Users.ToList();
}
public async Task <IActionResult> OnGetDeleteAsync()
{
var role = await _roleManager.FindByIdAsync(Id);
await _roleManager.DeleteAsync(role);
return RedirectToPage("/AdminDashboard");
}
public async Task<IActionResult> OnPostAsync()
{
if (roleName != null)
await _roleManager.CreateAsync(new IdentityRole(roleName.Trim()));
return RedirectToPage("/AdminDashboard");
}
public async Task<IActionResult> OnGetDeleteUserAsync()
{
var user = await _userManager.FindByIdAsync(Id);
await _userManager.DeleteAsync(user);
return RedirectToPage("/AdminDashboard");
}
public async Task<IActionResult> OnPostUpdateAsync()
{
var user = await _userManager.FindByIdAsync(Id);
await _userManager.RemovePasswordAsync(user);
await _userManager.AddPasswordAsync(user, NewPassword);
return RedirectToPage("/AdminDashboard");
}
}
}
Try it with this bit of code:
public async Task<IActionResult> OnPostUpdateAsync()
{
var user = await _userManager.FindByIdAsync(Id);
var token = await _userManager.GeneratePasswordResetTokenAsync(user)
var result = await _userManager.ResetPasswordAsync(user, token, NewPassword);
//validate result
if(result.Succeeded) {
return RedirectToPage("/AdminDashboard");
}
//handle errors
throw new Exception()
}
You don't actually need to validate the result, but things tend to go sideways. Always better to have error handling.
The generated token validates the action of changing the password against the system. If a user would want to reset their password, because they forgot theirs, you would also need to generate such a token, send it to their email and then let the user choose a new password.
Alternatively you could just use UserManager.UpdatePasswordHash:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.updatepasswordhash?view=aspnetcore-5.0
var result = await _userManager.UpdatePasswordHash(user, NewPassword, true|false);

How to add a value to a class if the class contains LIST<class>

I have a class graf.
public class Graf
{
public List<Point> first { get; set; }
public List<Point> second { get; set; }
}
This class contains List
public class Point
{
public int x { get; set; }
public int y { get; set; }
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
I need to add a Point into class Graf from index.cshtml:
#model WebApplication2.Models.Graf
<table>
<tr>
<td></td>
<td>
<div class="item">
<label>Y</label>
<input name="Y11" value="#Model.first" /> --------??
</div>
</td>
</tr>
</table>
<input type="submit" value="Send" />
But i dont now how i can input into Graf class Point?
How can I do it?
Ok. So let's start from a client-side code.I suppose that you have a next Index.cshtml view.
<!-- You use this code to display data from your model -->
<table>
<tr>
<td></td>
<td>
<div class="item">
<label>Y</label>
<input name="Y11" value="#Model.first" /> --------??
</div>
</td>
</tr>
</table>
Than you need a code that post new Point object from your view to controller.It could be like something like this:
<form asp-controller="Home" asp-action="InsertPoint" method="post">
X value: <input type="text" name="x"><br>
Y value: <input type="text" name="y"><br>
<input type="submit" value="Submit">
</form>
In your controller you should create action with following signature
[HttpPost]
public async Task<IActionResult> InsertPoint(Point point)
{
//Validation and insertion to list
return View();
}
NB
It's not an ideal solution. You could perform this task in many different ways. My aim, is just to show you the basic idea how it could be done. If you need more information you could start from this article
And of course, keep in mind that google is your good friend.

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 4 model coming back null

So I have a view that requires several different objects and lists of objects to be passed in and out that I have created a viewmodel for. My viewmodel looks like this
public class EditUserViewModel
{
public ManageUsersViewModel ManageUsersViewModel { get; set; }
public IEnumerable<StateModel> StateModel { get; set; }
}
The part I'm having trouble with is the StateModel which looks like this
public class StateModel
{
public bool IsChecked { get; set; }
public States States { get; set; }
public UsersInStates UsersInStates { get; set; }
}
and contains this
[Table("States")]
public class States
{
[Key]
public int StateId { get; set; }
public string State { get; set; }
}
[Table("UsersInStates")]
public class UsersInStates
{
[Key, Column(Order = 1)]
public int UserId { get; set; }
[Key, Column(Order = 2)]
public int StateId { get; set; }
public string LicenseNumber { get; set; }
}
In my view I'm more or less trying to loop through the states and take user input for UsersInStates. This is how I'm trying to accomplish it but my entire StateModel comes back null. Going into the view the StateModel.States has data and the UsersInStates does not. This is what it looks like in my view
#foreach (var state in Model.StateModel)
{
#Html.HiddenFor(m => state)
<tr>
<td>
#Html.CheckBoxFor(m => state.IsChecked)
</td>
<td>
#Html.Label(state.States.State)
</td>
<td>
#Html.EditorFor(m => state.UsersInStates.LicenseNumber)
</td>
</tr>
}
Any advice would be much appreciated. Everything displays as it should and the ManageUsersViewModel part works fine it's just the StateModel data coming back to the controller is null and I'm not exactly sure how to make this work the way I'd like it to.
This is what the generated html looks like for the start of the table and the first row as requested
<table style="margin-left:auto; margin-right:auto; text-align:center">
<input id="state" name="state" type="hidden" value="WebSiteNew.Models.StateModel" /> <tr>
<td>
<input data-val="true" data-val-required="The IsChecked field is required." id="state_IsChecked" name="state.IsChecked" type="checkbox" value="true" /> <input name="state.IsChecked" type="hidden" value="false" />
</td>
<td>
<label for="Alabama">Alabama</label>
</td>
<td>
<input class="text-box single-line" id="state_UsersInStates_LicenseNumber" name="state.UsersInStates.LicenseNumber" type="text" value="" />
</td>
</tr>
Answer:
Ok so to solve this I used a for loop as explained in both references listed in the answer below
#for (int i = 0; i < Model.StateModel.Count(); i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.StateModel[i].States.StateId)
#Html.HiddenFor(m => m.StateModel[i].States.State)
#Html.CheckBoxFor(m => m.StateModel[i].IsChecked)
</td>
<td>
#Html.Label(Model.StateModel[i].States.State)
</td>
<td>
#Html.EditorFor(m => m.StateModel[i].UsersInStates.LicenseNumber)
</td>
</tr>
}
Also a note to anyone looking at this, I had to change IEnumerable in my EditUsersViewModel to IList to allow for indexing.
So your issue is that the model binding isn't happening correctly.
Say you have these two models:
public class ParentModel
{
public string Name {get;set;}
public ChildModel Child {get;set;}
}
public class ChildModel
{
public string ChildName {get;set;}
public int SomeNumber {get;set;}
}
Your generated HTML (for your model binding to happen correctly) needs to look like this:
<input name="Name" value="(Not relevant to this example)"/>
<input name="Child.ChildName" value="(Not relevant to this example)" />
Note how the name field is structured - this is how MVC determines what input values map to which properties in your view model. With the nested property, the property name has to go in front of it.
With collections, it gets more complicated. The model binder needs to know which values go with which instance of a property.
For example, if we assume the ChildModel is of type IEnumerable in the previous example, your HTML might look something like this in order to model bind correctly:
<input name="Name" value="(Not relevant to this example)"/>
<input name="Child[0].ChildName" value="(Not relevant to this example)" />
<input name="Child[0].SomeNumber" value="(Not relevant to this example)"/>
<input name="Child[1].ChildName" value="(Not relevant to this example)" />
<input name="Child[1].SomeNumber" value="(Not relevant to this example)"/>
Take a look at these for how to fix it:
http://seesharpdeveloper.blogspot.com/2012/05/mvc-model-binding-to-list-of-complex.html
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Edit - It's also important to note that when the Html Helpers generate the name value, it's based on the lambda value that is passed in. So
#Html.CheckBoxFor(m => state.IsChecked)
will generate the following name
name="state.IsChecked"
Since you're within a foreach, you're getting the wrong value for the name.
What are you trying to accomplish with the #Html.HiddenFor(m => state) - from the rendered HTML, that looks like your culprit right there. Would #Html.HiddenFor(m => state.StateId) be more appropriate?
Also, you could throw that into the first <td> element since it is hidden and will keep your HTML valid.

Resources