UpdateAsync does not save the data - asp.net

I have edit page, which should to edit data of IdentityUser. On this page, I have a form with method="post" and also I have a controller, that have a method with [HttpPost] attribute. So, I try to use _userManager.UpdateAsync(user) but unfortunately it does not work. I have checked on succeeded updating user data, but I have no idea why data not updates. Below you will see the code of View and Controllers.
Edit View
#model BookStore.ViewModels.EditViewModel
#{
ViewData["Title"] = "Edit Info";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<form method="post" asp-controller="Profile" asp-action="EditInfo">
<h3 class="registerTitle">Edit Data</h3>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<div><label asp-for="Email"></label></div>
<input asp-for="Email" placeholder="example#mail.com" />
<span class="text-danger" asp-validation-for="Email"></span>
</div>
<!--Birthday inputs-->
<div class="form-group">
<div>
<label type="text" asp-for="birthDate"></label>
</div>
#Html.DropDownList("Month", Enumerable.Range(1, 12).Select(i => new SelectListItem { Value = i.ToString(), Text = System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat.GetMonthName(i) }), "Select Month")
#Html.DropDownList("Day", Enumerable.Range(1, 31).Select(i => new SelectListItem { Value = i.ToString(), Text = i.ToString() }), "Select Day")
#Html.DropDownList("Year", Enumerable.Range(1900, 109).Select(i => new SelectListItem { Value = i.ToString(), Text = i.ToString() }), "Select Year")
</div>
<!--First Name input-->
<div class="form-group">
<div>
<label type="text" asp-for="FirstName"></label>
</div>
<input type="text" asp-for="FirstName" placeholder="First Name" />
<span class="text-danger" asp-validation-for="FirstName"></span>
</div>
<!--Last Name input-->
<div class="form-group">
<div>
<label type="text" asp-for="LastName"></label>
</div>
<input type="text" asp-for="LastName" placeholder="First Name" />
<span class="text-danger" asp-validation-for="LastName"></span>
</div>
<!--User Name input-->
<div class="form-group">
<div>
<label type="text" asp-for="UserName"></label>
</div>
<input type="text" asp-for="UserName" placeholder="Some_username" />
<span class="text-danger" asp-validation-for="UserName"></span>
</div>
<div class="row">
<div class="col-md"></div>
<div class="col-md"><input type="submit" value="Save"/></div>
<div class="col-md"></div>
</div>
</form>
ProfileController
[Route("Info/Edit/{username}")]
[HttpGet]
public async Task<IActionResult> Edit(string username)
{
var user = await _userManager.FindByNameAsync(username);
EditViewModel edit = new EditViewModel()
{
FirstName = user.FirstName,
LastName = user.LastName,
Month = user.birthDate.Month,
Year = user.birthDate.Year,
Day = user.birthDate.Day,
Email = user.Email,
UserName = user.UserName
};
return View(edit);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditInfo(EditViewModel editModel)
{
var user = await _userManager.FindByNameAsync(User.Identity.Name);
if (user == null) return BadRequest("user is not found");
user.FirstName = editModel.FirstName;
user.LastName = editModel.LastName;
user.UserName = editModel.UserName;
user.Year = editModel.birthDate.Year;
user.Month = editModel.birthDate.Month;
user.Day = editModel.birthDate.Day;
user.Email = editModel.Email;
IdentityResult result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return Content("Ok");
}
return View(editModel);
}
EditViewModel class
public class EditViewModel
{
[EmailAddress]
public string Email { get; set; }
[Display(Name = "Username")]
public string UserName { get; set; }
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "Date of birth")]
[DataType(DataType.Date)]
public DateTime birthDate
{
get
{
return new DateTime(Year,Month,Day);
}
}
public int Day { get; set; }
public int Month { get; set; }
public int Year { get; set; }
}

Explicitly add an antiforgery token to a form element
<form asp-action="EditInfo" asp-controller="Profile" method="post">
#Html.AntiForgeryToken()
<!-- ... -->
</form>
and fix the action code, return badrequest if user is null
var user = await _userManager.FindByNameAsync(editModel.UserName);
if(user == null) return BadRequest("user is not found");
user.FirstName = editModel.FirstName;
....
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return Content("Ok");
}
......
and I can see some bugs in the view too. For example
#Html.DropDownList("Day", Enumerable.Range(1, 31).Select(i => new SelectListItem { Value = i.ToString(), Text = i.ToString() }), "Select Day")
should be
#Html.DropDownListFor(model=> Model.Day, Enumerable.Range(1, 31).Select(i => new SelectListItem { Value = i.ToString(), Text = i.ToString() }), "Select Day")
I can see in the post action
user.Day = editModel.birthDate.Day;
it should be
user.Day = editModel.Day;
but i see this in Get action
editModel.Month = user.birthDate.Month,
I am amazed how you could compile it.

Related

Model coming through as null in controller

I have a relatively simple form that's posting to a controller, yet the model is coming through as null every time and I don't understand why. I'm using Umbraco CMS (so please ignore any Umbraco references if they're not relevant) and jquery combined with an image plugin (Filepond) to post the data. This is my model:
public class PropertyViewModel: PublishedContentWrapped
{
public PropertyViewModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback)
{
this.Countries = GetCountries();
}
[Required]
public string Country { get; set; }
[Required]
public string DisplayName { get; set; }
[Required]
public string Summary { get; set; }
[Required]
public string Description { get; set; }
[Required(ErrorMessage = "Please select at least one image")]
public List<IFormFile> Images { get; set; }
public SelectList Countries { get; set; }
public SelectList GetCountries()
{
List<string> countryNames = Bia.Countries.Iso3166.Countries.GetAllActiveDirectoryNames();
var countries = new List<SelectListItem>();
foreach (var country in countryNames)
countries.Add(new SelectListItem { Text = country, Value = country });
return new SelectList(countries, "Text", "Value");
}
}
Here is the controller:
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateUmbracoFormRouteString]
public async Task<IActionResult> HandleAddProperty(PropertyViewModel model)
{
if (ModelState.IsValid == false)
{
return CurrentUmbracoPage();
}
// Carry on execution..
}
Finally, here is my view. I should mention that model is a new instance of the PropertyViewModel class:
#using (Html.BeginUmbracoForm<PropertySurfaceController>("HandleAddProperty", null, new { #id = "form-add" }))
{
<div asp-validation-summary="All" class="text-danger"></div>
<div class="mb-3">
<select asp-for="#Model.Country" asp-items="Model.Countries" class="form-select">
<option value="">Select country</option>
</select>
<span asp-validation-for="#Model.Country" class="form-text text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="#Model.DisplayName" class="form-label"></label>
<input asp-for="#Model.DisplayName" class="form-control" aria-required="true" />
<span asp-validation-for="#Model.DisplayName" class="form-text text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="#Model.Summary" class="form-label"></label>
<textarea asp-for="#Model.Summary" class="form-control" aria-required="true"></textarea>
<span asp-validation-for="#Model.Summary" class="form-text text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="#Model.Description" class="form-label"></label>
<textarea asp-for="#Model.Description" class="form-control" aria-required="true"></textarea>
<span asp-validation-for="#Model.Description" class="form-text text-danger"></span>
</div>
<div class="mb-3">
<input type="file" class="filepond" id="file" name="filepond" multiple data-max-file-size="5MB" data-max-files="10" accept="image/png, image/jpeg, image/gif">
</div>
<button type="submit" class="btn btn-primary" id="submit">Submit</button>
}
<script>
$(document).ready(function(e){
FilePond.registerPlugin(
FilePondPluginImageResize,
FilePondPluginFileValidateSize
);
var pond = FilePond.create(
document.querySelector('#file'), {
allowMultiple: true,
instantUpload: false,
allowProcess: false,
imageResizeTargetWidth: 1280,
imageResizeUpscale: false,
imageResizeMode: 'contain'
});
$("#form-add").submit(function (e) {
e.preventDefault();
var formdata = new FormData(this);
// append FilePond files into the form data
var pondFiles = pond.getFiles();
for (var i = 0; i < pondFiles.length; i++) {
formdata.append('Images', pondFiles[i].file);
}
console.log('formdata', formdata);
$.ajax({
url: "/umbraco/surface/PropertySurface/HandleAddProperty",
data: formdata,
dataType: 'JSON',
processData: false,
contentType: false,
method:"post"
}).done(function (response) {
// todo
});
})
});
</script>
When I inspect in Chrome to see what data is being sent to the server I can see the following:
So I can see the fields are matching the model, yet it's still coming through as null. I can't figure out where it's failing.
I've tried using [FromBody] and [FromForm] with the controller parameter, but it made no difference.
Is anyone able to spot where I'm going wrong?
I think the second parameter in BeginUmbracoForm is supposed to be the model, not null. So:
#using (Html.BeginUmbracoForm<PropertySurfaceController>("HandleAddProperty", new { #id = "form-add" }))
{ ... }

Dropdown Data Binding Problem in ASP.NET Core 6 MVC

I am using SelectListItem in the controller for binding my dropdown data. All the dropdown options are showing perfectly in the dropdown list, but when I try to save, the problem occurs. It's not adding the dropdown options data rather than its adding dropdown data's id.
All the related models, controller and views are shown here:
BuyerSelectList model class:
public class BuyerSelectList
{
[Key]
public int Id { get; set; }
[DisplayName("BUYER")]
public string Buyer { get; set; }
}
ItemSelectList model class:
public class ItemSelectList
{
[Key]
public int Id { get; set; }
[DisplayName("ITEM")]
public string Item { get; set; }
}
BTBNewLien2 model class:
public class BTBNewLien2
{
public int Id { get; set; }
[Required]
[DisplayName("Buyer")]
public int BuyerSelectListId { get; set; }
[ForeignKey("BuyerSelectListId")]
[ValidateNever]
public BuyerSelectList BuyerSelectList { get; set; }
[Required]
[DisplayName("Item")]
public int ItemSelectListId { get; set; }
[ForeignKey("ItemSelectListId")]
[ValidateNever]
public ItemSelectList ItemSelectList { get; set; }
}
BTBNewLien2 controller (here I added all the data binding functionalities for my dropdown):
namespace CommercialCalculatorWeb.Areas.Admin.Controllers
{
public class BTBNewLien2Controller : Controller
{
private readonly IUnitOfWork _unitOfWork;
public BTBNewLien2Controller(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public IActionResult Index()
{
IEnumerable<BTBNewLien2> objBTBNewLienList = _unitOfWork.BTBNewLien2.GetAll();
return View(objBTBNewLienList);
}
public IActionResult Create()
{
BTBNewLien2 btbNewLien2 = new();
IEnumerable<SelectListItem> BuyerSelectList = _unitOfWork.Buyer.GetAll().Select(
c => new SelectListItem
{
Text = c.Buyer,
Value = c.Id.ToString()
});
IEnumerable<SelectListItem> ItemSelectList = _unitOfWork.Item.GetAll().Select(
c => new SelectListItem
{
Text = c.Item,
Value = c.Id.ToString()
});
ViewBag.BuyerSelectList = BuyerSelectList;
ViewBag.ItemSelectList = ItemSelectList;
return View(btbNewLien2);
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(BTBNewLien2 obj)
{
if (ModelState.IsValid)
{
_unitOfWork.BTBNewLien2.Add(obj);
_unitOfWork.Save();
TempData["success"] = "Row Created Successfully!";
return RedirectToAction("Index");
}
return View(obj);
}
}
}
BTBNewLien2 create view:
#model CommercialCalculator.Models.BTBNewLien2
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>BTBNewLien2</h4>
<hr />
<div class="row ml-6">
<div class="col-md-4">
<form asp-action="Create">
<div class="form-group">
<label asp-for="BuyerSelectListId" class="control-label">Buyer</label>
<select asp-for="BuyerSelectListId" asp-items="ViewBag.BuyerSelectList" class="form-control">
<option disabled selected>--Select Buyer--</option>
</select>
</div>
<div class="form-group">
<label asp-for="ItemSelectListId" class="control-label">Item</label>
<select asp-for="ItemSelectListId" asp-items="ViewBag.ItemSelectList" class="form-control">
<option disabled selected>--Select Item--</option>
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
BTBNewLien2 index view:
#model IEnumerable<CommercialCalculator.Models.BTBNewLien2>
#{
ViewData["Title"] = "Index";
}
<table class="table table-bordered table-hover table-sm align-middle m-0" id="header">
<tr class="m-0" style="text-align:center;background-color: #17A2B8">
<th width="20%">
#Html.DisplayNameFor(model => model.BuyerSelectList)
</th>
<th>
#Html.DisplayNameFor(model => model.ItemSelectList)
</th>
</tr>
#foreach (var BTBNewLien2 in Model)
{
<tr class="m-0">
<td>
#Html.DisplayFor(modelItem => BTBNewLien2.BuyerSelectList)
</td>
<td>
#Html.DisplayFor(modelItem => BTBNewLien2.ItemSelectList)
</td>
</tr>
}
</table>
Try this way:
#Html.DropDownList("ItemSelectListId", new SelectList(ViewBag.ItemSelectListId, "Text", "Text"), "-- Select Item --", new { required = true, #class = "form-control" })
In my code, it works fine:
Controller:
[HttpGet]
public IActionResult Create()
{
List<SelectListItem> test = new()
{
new SelectListItem { Value = "1", Text = "test1" },
new SelectListItem { Value = "2", Text = "test2" },
new SelectListItem { Value = "3", Text = "test3" },
new SelectListItem { Value = "4", Text = "test4" }
};
ViewBag.ItemSelectListId = test;
return View();
}
[HttpPost]
public IActionResult Create(Test test)
{
return View();
}
View:
<div class="row ml-6">
<div class="col-md-4">
<form asp-action="Create">
<div class="form-group">
<label asp-for="ItemSelectListId" class="control-label">Buyer</label>
#Html.DropDownList("ItemSelectListId", new SelectList(ViewBag.ItemSelectListId, "Text", "Text"), "-- Select Item --", new { required = true, #class = "form-control" })
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
Test Result:

ArgumentNullException: Value cannot be null. (Parameter 'items') Register Form using Identity with Role Authorization

Im just starting ASP.NET 5 Core MVC. I used identity for register. When I submit the register form, Model.isValid returns false in the post of razor page. The only thing that gets posted is the dropdown Item, all others are null. If I remove the role dropdown from the register and then submit it works fine.(Input.Name is the role)
namespace SoftcodeWebGem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class RegisterModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
private readonly RoleManager<IdentityRole> _roleManager;
public RegisterModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender,
RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
_roleManager = roleManager;
}
[BindProperty]
public InputModel Input { get; set; }
public string ReturnUrl { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public class InputModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[DataType(DataType.Text)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string Name { get; set; }
}
//public void OnGet(string returnUrl = null)
//{
// ViewData["roles"] = _roleManager.Roles.ToList();
// ReturnUrl = returnUrl;
//}
public async Task OnGetAsync(string returnUrl = null)
{
ViewData["roles"] = _roleManager.Roles.ToList();
ReturnUrl = returnUrl;
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
var role = _roleManager.FindByIdAsync(Input.Name).Result;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email, FirstName = Input.FirstName, LastName = Input.LastName };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
await _userManager.AddToRoleAsync(user, role.Name);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
}
else
{
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
ViewData["roles"] = _roleManager.Roles.ToList();
}
// If we got this far, something failed, redisplay form
return Page();
}
}
}
ScreenShot of The Debug
The below code is the Select element
#page
#model RegisterModel
#{
ViewData["Title"] = "Register";
var roles = (List<IdentityRole>)ViewData["roles"];
}
<div class="container">
<div style="padding-top:80px;"></div>
<div class="container pt-5 pb-3" style="background-color: white;
width:500px; height:auto; border-radius:10px;">
<div class="text-center">
<h3>Softcode Gem Solution</h3>
<h6><img src="~/images/icons/add_user_male_32px.png" alt="Register" />Register with your Credentials</h6>
</div>
<form asp-route-returnUrl="#Model.ReturnUrl" method="post">
#Html.AntiForgeryToken()
<div class="form-floating mb-3 ">
<input asp-for="Input.FirstName" type="text" class="form-control" id="firstname" name="firstname" placeholder="First Name" style="border-radius: 5px; ">
<label asp-for="Input.FirstName"><img src="~/images/icons/name_24px.png" />First Name</label>
<span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.LastName" type="text" class="form-control" id="lastname" name="lastname" placeholder="Last Name" style="border-radius: 5px; ">
<label asp-for="Input.LastName"><img src="~/images/icons/name_24px.png" />Last Name</label>
<span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" type="email" class="form-control" id="email" name="email" placeholder="Email" style="border-radius: 5px; ">
<label asp-for="Input.Email"><img src="~/images/icons/email_24px.png" />Email address</label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-floating mb-3 pb-2">
<input asp-for="Input.Password" type="password" class="form-control" id="password" name="password" placeholder="Password" style="border-radius: 5px;">
<label asp-for="Input.Password"><img src="~/images/icons/password_24px.png" />Password</label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-floating mb-3 pb-2">
<input asp-for="Input.ConfirmPassword" type="password" class="form-control" id="confirmpassword" name="confirmpassword" placeholder="Confirm Password" style="border-radius: 5px;">
<label asp-for="Input.ConfirmPassword"><img src="~/images/icons/password_24px.png" />Confirm Password</label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<div class="form-control mb-3 pb-2">
<label asp-for="Input.Name"></label>
<select asp-for="Input.Name" class="form-control" asp-items='new SelectList((List<IdentityRole>)ViewData["roles"], "Id", "Name")'></select>
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<button class="w-100 btn btn-lg btn-primary " type="submit">Register</button>
<a asp-page="./Login" asp-route-returnUrl="#Model.ReturnUrl" style="text-decoration:none;"><p class="mt-3 mb-2 text-muted ">Already Registered? Login</p></a>
</form>
</div>
#section Scripts {
<partial name="_ValidationScriptsPartial" />
}
There are many questions similar to this but none of them use identity or that what I found. Please help
In your code it looks like there is a conflict in Name as in your Register.chtml
First change the public string Name { get; set; } code in the RegisterModel to
[Display(Name = "User Role")]
public string UserRole { get; set; }
Then look at this part of your code input.Name and the Name in the SelectList:
<div class="form-control mb-3 pb-2">
<label asp-for="Input.Name"></label>
<select asp-for="Input.Name" class="form-control" asp-items='new SelectList((List<IdentityRole>)ViewData["roles"], "Id", "Name")'></select>
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
Just try changing to this:
<div class="form-control mb-3 pb-2">
<label asp-for="Input.UserRole"></label>
<select asp-for="Input.UserRole" class="form-control" asp-items='new SelectList((List<IdentityRole>)ViewData["roles"], "Id", "Name")'></select>
<span asp-validation-for="Input.UserRole" class="text-danger"></span>
</div>
`
fix action OnGetAsync, add Roles
....
ViewBag.Roles = _roleManager.Roles.Select (i=> new SelectListItem {
Value= i.Id.ToString(),
Text= i.Name
}).ToList();
....
and view
<select asp-for="Input.Name" class="form-control" asp-items="#ViewBag.Roles"></select>
And I can't see your Input but something tells me that instead of Input.Name you have to use Input.Id or create it.

Add Roles to User using checkboxes

I am developing a Razor application and I need the Admin to assign or update roles of the Users. I am using a checkbox to carry out this action. So far, I have been able to populate the view to show the users and their respective roles ticked in the checkbox, but I haven't been able to update their roles, as anytime a checkbox is ticked to add a role, it doesn't add the role to the user. I think this line is the culprit: var selectedRoles = model.Where(x => x.Selected).Select(y => y.RoleName);.
This is the Model:
public class ManageUserRolesViewModel
{
public string RoleId { get; set; }
public string RoleName { get; set; }
public bool Selected { get; set; }
}
This is the Page:
<form method="post">
<div class="card">
<div class="card-header">
<h2>Manage User Roles</h2>
Add / Remove Roles for #Model.UserID
</div>
<div class="card-body">
#foreach (var x in Model.UserRoles)
{
<div class="form-check m-1">
<input type="hidden" asp-for="#x.RoleId" />
<input type="hidden" asp-for="#x.RoleName" />
<input asp-for="#x.Selected" class="form-check-input" />
<label class="form-check-label" asp-for="#x.Selected">
#x.RoleName
</label>
</div>
}
<div asp-validation-summary="All" class="text-danger"></div>
</div>
<div class="card-footer">
<input type="submit" value="Update" class="btn btn-primary" style="width:auto" />
<a asp-page="/Account/UserManagement/UserList" class="btn btn-primary" style="width:auto">Cancel</a>
</div>
</div>
</form>
This is the Page Model:
public class ManageModel : PageModel
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly BankAssesmentApplicationIdentityDbContext _db;
private readonly UserManager<IdentityUser> _userManager;
public ManageModel(
RoleManager<IdentityRole> roleManager,
UserManager<IdentityUser> userManager,
BankAssesmentApplicationIdentityDbContext db)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
}
public IList<ManageUserRolesViewModel> UserRoles = new List<ManageUserRolesViewModel>();
public string UserID { get; set; }
public async Task<IActionResult> OnPostAsync(List<ManageUserRolesViewModel> model, string userId)
{
UserID = userId;
if (ModelState.IsValid)
{
IdentityUser user = await _userManager.FindByNameAsync(userId);
if (user == null)
{
return Page();
}
var roles = await _userManager.GetRolesAsync(user);
var result = await _userManager.RemoveFromRolesAsync(user, roles);
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot remove user existing roles");
return Page();
}
var selectedRoles = model.Where(x => x.Selected).Select(y => y.RoleName);
await _userManager.AddToRolesAsync(user, selectedRoles);
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot add selected roles to user");
return Page();
}
return RedirectToPage("/Account/UserManagement/UserList");
}
return Page();
}
public async Task<IActionResult> OnGetAsync(string userId)
{
UserID = userId;
var user = await _userManager.FindByEmailAsync(userId);
if (user == null)
{
return Page();
}
var model = new List<ManageUserRolesViewModel>();
foreach (var role in _roleManager.Roles.ToList())
{
ManageUserRolesViewModel roles = new ManageUserRolesViewModel
{
RoleId = role.Id,
RoleName = role.Name,
};
UserRoles.Add(roles);
if (await _userManager.IsInRoleAsync(user, role.Name))
{
roles.Selected = true;
}
else
{
roles.Selected = false;
}
model.Add(roles);
}
return Page();
}
}
Firsly,you need know that for each property of the complex type, model binding looks through the sources for the name pattern prefix.property_name. If nothing is found, it looks for just property_name without the prefix.Your backend wants to receive a list model,so what you pass should be [index].PropertyName.But what you did will result in serveral inputs with the same name,the model binding system could not match the value for the list.
Then you need know that asp-for="#x.Selected" will generate the value for checkbox,but it will not change the value when you change the checkbox state,you need create a click event to change the value:
<input asp-for="#x.Selected" onclick="$(this).val(this.checked ? true : false)"/>
What you need change like below:
<form method="post">
<div class="card">
<div class="card-header">
<h2>Manage User Roles</h2>
Add / Remove Roles for #Model.UserID
</div>
<div class="card-body">
#*Begin change*#
#{ int i = 0;}
#foreach (var x in Model.UserRoles)
{
<div class="form-check m-1">
<input type="hidden" asp-for="#x.RoleId" name="[#i].RoleId"/>
<input type="hidden" asp-for="#x.RoleName" name="[#i].RoleName"/>
<input asp-for="#x.Selected" name="[#i].Selected" class="form-check-input" onclick="$(this).val(this.checked ? true : false)"/>
<label class="form-check-label" asp-for="#x.Selected">
#x.RoleName
</label>
</div>
i++;
}
#*End change*#
<div asp-validation-summary="All" class="text-danger"></div>
</div>
<div class="card-footer">
<input type="submit" value="Update" class="btn btn-primary" style="width:auto" />
<a asp-page="/Account/UserManagement/UserList" class="btn btn-primary" style="width:auto">Cancel</a>
</div>
</div>
</form>
Result:
#user:3843256 have a look at this sample it works ok, the only difference is you are using for each, that means bind does not take place, change to counter based index binding
Checkbox list binding

How do I render a group of checkboxes using MVC 4 and View Models (strongly typed)

I'm rather new to the ASP.net MVC world and I'm trying to figure out how to render a group of checkboxes that are strongly typed to a view model. In webforms I would just use the checkboxlist control but im a bit lost with MVC.
I'm building a simple contact form for a wedding planning business and need to pass whatever checkbox values the user selects to my controller.
The form checkboxes need to look like this:
Your help would be greatly appreciated. Thanks!
Here's what I have so far.
CONTROLLER
[HttpPost]
public ActionResult Contact(ContactViewModel ContactVM)
{
if (!ModelState.IsValid)
{
return View(ContactVM);
}
else
{
//Send email logic
return RedirectToAction("ContactConfirm");
}
}
VIEW MODEL
public class ContactViewModel
{
[Required]
public string Name { get; set; }
[Required]
public string Phone { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[Required]
public string Subject { get; set; }
public IEnumerable<SelectListItem> SubjectValues
{
get
{
return new[]
{
new SelectListItem { Value = "General Inquiry", Text = "General Inquiry" },
new SelectListItem { Value = "Full Wedding Package", Text = "Full Wedding Package" },
new SelectListItem { Value = "Day of Wedding", Text = "Day of Wedding" },
new SelectListItem { Value = "Hourly Consultation", Text = "Hourly Consultation" }
};
}
}
//Not sure what I should do for checkboxes...
}
VIEW
#model NBP.ViewModels.ContactViewModel
#{
ViewBag.Title = "Contact";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
<div id="ContactContainer">
<div><span class="RequiredField">* </span>Your Name:</div>
<div>
#Html.TextBoxFor(model => model.Name)
</div>
<div><span class="RequiredField">* </span>Your Phone:</div>
<div>
#Html.TextBoxFor(model => model.Phone)
</div>
<div><span class="RequiredField">* </span>Your Email:</div>
<div>
#Html.TextBoxFor(model => model.Email)
</div>
<div>Subject:</div>
<div>
#Html.DropDownListFor(model => model.Subject, Model.SubjectValues)
</div>
<div>Vendor Assistance:</div>
<div>
<!-- CHECKBOXES HERE -->
</div>
<div>
<input id="btnSubmit" type="submit" value="Submit" />
</div>
</div>
}
You could enrich your view model:
public class VendorAssistanceViewModel
{
public string Name { get; set; }
public bool Checked { get; set; }
}
public class ContactViewModel
{
public ContactViewModel()
{
VendorAssistances = new[]
{
new VendorAssistanceViewModel { Name = "DJ/BAND" },
new VendorAssistanceViewModel { Name = "Officiant" },
new VendorAssistanceViewModel { Name = "Florist" },
new VendorAssistanceViewModel { Name = "Photographer" },
new VendorAssistanceViewModel { Name = "Videographer" },
new VendorAssistanceViewModel { Name = "Transportation" },
}.ToList();
}
[Required]
public string Name { get; set; }
[Required]
public string Phone { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[Required]
public string Subject { get; set; }
public IEnumerable<SelectListItem> SubjectValues
{
get
{
return new[]
{
new SelectListItem { Value = "General Inquiry", Text = "General Inquiry" },
new SelectListItem { Value = "Full Wedding Package", Text = "Full Wedding Package" },
new SelectListItem { Value = "Day of Wedding", Text = "Day of Wedding" },
new SelectListItem { Value = "Hourly Consultation", Text = "Hourly Consultation" }
};
}
}
public IList<VendorAssistanceViewModel> VendorAssistances { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new ContactViewModel());
}
[HttpPost]
public ActionResult Index(ContactViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
//Send email logic
return RedirectToAction("ContactConfirm");
}
}
View:
#using (Html.BeginForm())
{
<div id="ContactContainer">
<div><span class="RequiredField">* </span>Your Name:</div>
<div>
#Html.TextBoxFor(model => model.Name)
</div>
<div><span class="RequiredField">* </span>Your Phone:</div>
<div>
#Html.TextBoxFor(model => model.Phone)
</div>
<div><span class="RequiredField">* </span>Your Email:</div>
<div>
#Html.TextBoxFor(model => model.Email)
</div>
<div>Subject:</div>
<div>
#Html.DropDownListFor(model => model.Subject, Model.SubjectValues)
</div>
<div>Vendor Assistance:</div>
<div>
#for (int i = 0; i < Model.VendorAssistances.Count; i++)
{
<div>
#Html.HiddenFor(x => x.VendorAssistances[i].Name)
#Html.CheckBoxFor(x => x.VendorAssistances[i].Checked)
#Html.LabelFor(x => x.VendorAssistances[i].Checked, Model.VendorAssistances[i].Name)
</div>
}
</div>
<div>
<input id="btnSubmit" type="submit" value="Submit" />
</div>
</div>
}
Use a string array in your view model. You can then use the helper I hacked together. if you don't want to use the helper and the enum then see the actual Html at the bottom. The binder will return a string array with only the selected string values in it. if none are selected it returns a null value for your array. You must account for that, you have been warned :)
View Model:
[Display(Name = "Which Credit Cards are Accepted:")]
public string[] CreditCards { get; set; }
Helper:
public static HtmlString CheckboxGroup<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> propertySelector, Type EnumType)
{
var groupName = GetPropertyName(propertySelector);
var modelValues = ModelMetadata.FromLambdaExpression(propertySelector, htmlHelper.ViewData).Model;//propertySelector.Compile().Invoke(htmlHelper.ViewData.Model);
StringBuilder literal = new StringBuilder();
foreach (var value in Enum.GetValues(EnumType))
{
var svalue = value.ToString();
var builder = new TagBuilder("input");
builder.GenerateId(groupName);
builder.Attributes.Add("type", "checkbox");
builder.Attributes.Add("name", groupName);
builder.Attributes.Add("value", svalue);
var contextValues = HttpContext.Current.Request.Form.GetValues(groupName);
if ((contextValues != null && contextValues.Contains(svalue)) || (modelValues != null && modelValues.ToString().Contains(svalue)))
{
builder.Attributes.Add("checked", null);
}
literal.Append(String.Format("</br>{1} <span>{0}</span>", svalue.Replace('_', ' '),builder.ToString(TagRenderMode.Normal)));
}
return (HtmlString)htmlHelper.Raw(literal.ToString());
}
private static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
var body = propertySelector.Body.ToString();
var firstIndex = body.IndexOf('.') + 1;
return body.Substring(firstIndex);
}
HTML:
#Html.CheckboxGroup(m => m.CreditCards, typeof(VendorCertification.Enums.CreditCardTypes))
Use this if helper extensions scare you:
<input id="CreditCards" name="CreditCards" type="checkbox" value="Visa"
#(Model.CreditCards != null && Model.CreditCards.Contains("Visa") ? "checked=true" : string.Empty)/>
<span>Visa</span><br />
<input id="CreditCards" name="CreditCards" type="checkbox" value="MasterCard"
#(Model.CreditCards != null && Model.CreditCards.Contains("MasterCard") ? "checked=true" : string.Empty)/>
<span>MasterCard</span><br />
For me this works too, and I think this is the simplest (reading the previous answers).
The viewmodel has a string[] for the check boxes.
public string[] Set { get; set; }
The view has this code, and you can repeat the input as many times you need. name, id of the input control has to match the name of the property of the viewmodel.
<div class="col-md-3">
<div class="panel panel-default panel-srcbox">
<div class="panel-heading">
<h3 class="panel-title">Set</h3>
</div>
<div class="panel-body">
<div class="form-group-sm">
<label class="control-label col-xs-3">1</label>
<div class="col-sm-8">
<input type="checkbox" id="Set" name="Set" value="1" />
</div>
<label class="control-label col-xs-3">2</label>
<div class="col-sm-8">
<input type="checkbox" id="Set" name="Set" value="2" />
</div>
</div>
</div>
</div>
</div>
On the post method the Set variable is an array, having the checked value(s).

Resources