Add Roles to User using checkboxes - asp.net

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

Related

How to fix Invalid OperationException: Multiple constructors accepting all given argument types have been found in type 'System.Collections.Generic

I have seen variations to this question asked. But none of the answers seem to help me.
When trying to view a list of roles in my view after hitting the AddOrRemoveUsers button(see picture)
I get the following error message.
I cant find where the multiple constructors error seem to be. Am I missing something.
#page
#using ThreeTierAdvisementApp.Areas.Identity.Pages.Account.Administration
#using ThreeTierAdvisementApp.Data
#model List<UserRole>
<form method="post">
<div class="card">
<div class="card-header">
<h2>Add or remove users from this role</h2>
</div>
<div class="card-body">
#for(int i = 0; i<Model.Count; i++){
<div class="form-check m-1">
<input asp-for="#Model[i].IsSelected" class="form-check-input" />
<label class="form-check-label">
#Model[i].UserName
</label>
</div>
}
</div>
<div class="card-footer">
<input type="submit" value="Update" class="btn btn-primary"
style="width:auto" />
<a asp-action="EditRole" asp-route-id="UserId"
class="btn btn-primary" style="width:auto">Cancel</a>
</div>
</div>
</form>
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using ThreeTierAdvisementApp.Data;
namespace ThreeTierAdvisementApp.Areas.Identity.Pages.Account.Administration
{
public class EditUsersInRoleModel : PageModel
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<DefaultUser> _userManager;
public EditUsersInRoleModel(RoleManager<IdentityRole> roleManager, UserManager<DefaultUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
[BindProperty]
public UserRole RoleView { get; set; }
public async Task<IActionResult> OnGet(string roleId)
{
RoleView = new UserRole { UserId = roleId };
var role = await _roleManager.FindByIdAsync(roleId);
if (role == null)
{
return NotFound();
}
var model = new List<UserRole>();
foreach (var user in _userManager.Users)
{
var userRoleViewModel = new UserRole
{
UserId = user.Id,
UserName = user.UserName,
};
if (await _userManager.IsInRoleAsync(user, role.Name))
{
userRoleViewModel.IsSelected = true;
}
else
{
userRoleViewModel.IsSelected = false;
}
model.Add(userRoleViewModel);
}
return Page();
}
}
}
using Microsoft.AspNetCore.Identity;
using System.Security.Principal;
namespace ThreeTierAdvisementApp.Data
{
public class UserRole
{
public string UserId { get; set; }
public string UserName { get; set; }
public bool IsSelected { get; set; }
}
}
I am using asp.net 6 Razor page pattern but all the examples online are using the MVC pattern. Would appreciate some feedback on how to handle this.

Razor PageModel RedirectToRouteResult does not work when calling OnPost

I'm trying to implement simple Asp.net core web application login flow.
The LoginModel
namespace Trading_System.UI.Pages.Account
{
public class LoginModel : PageModel
{
[BindProperty]
public string Username { get; set; }
[BindProperty]
public string Password { get; set; }
public string ReturnUrl { get; set; }
public string ErrorMessage { get; set; }
private IUserManager m_userManager;
public LoginModel(IUserManager userManager)
{
m_userManager = userManager;
}
public void OnGet(string returnUrl)
{
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPost()
{
var user = m_userManager.GetUser(Username, Password);
if (user == null)
{
ErrorMessage = "Username or password are invalid.";
return Page();
}
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.UserName)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return new RedirectToRouteResult(ReturnUrl);
}
public async Task<IActionResult> OnPostLogout(string returnUrl)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return new RedirectToPageResult("/index");
}
}
}
The cshtml code of Login
#page
#model Account.LoginModel
#{
ViewData["Title"] = "Login";
}
<div class="login-page">
<div class="page-header">
<h1>Login</h1>
</div>
<div class="row">
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Please enter your credentials</h3>
</div>
<div class="panel-body">
<form method="post">
<fieldset>
<div class="form-group">
<label asp-for="Username"></label>
<input class="form-control" placeholder="Username" asp-for="Username" autofocus>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off">
</div>
<div class="form-group">
<button class="btn btn-primary">Login</button>
</div>
#if(!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="alert alert-warning">#Model.ErrorMessage</div>
}
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
I'm setting the ReturnUrl property when OnGet is called (for example "https://localhost:44389/Account/Login?ReturnUrl=%2FSecretPage") But I've also tried to set it manually so the problem is not there.
After I'm pressing Login I get redirected to the login page no matter what is the ReturnUrl given (I can see in chrome the 302 response).
The url after the submit is "https://localhost:44389/Account/Login?Length=14"
What am I doing wrong? I just want to redirect back to the route I was at before redirected to the login page.
RedirectToRouteResult does a redirect to a route where a route is a named route that is configured through the MVC router.
As your example shows, the ReturnUrl that gets passed to your login action is a relative URL though: /Account/Login?ReturnUrl=%2FSecretPage. So ReturnUrl is "/SecretPage".
A relative URL is usually equal to a named route, so using RedirectToRoute will not work here. What you can do instead is return a LocalRedirectResult:
return LocalRedirect(ReturnUrl);

Populating data from last row in form when creating a new entry

I have a form to create new data entries for comments. Creating completely new entries works fine. However, when I have already created one entry for my entity I want to populate the data from the last entry in my form.
I have tried to modify the OnGet action to include the data from the last entry. I copied the OnGet code from the Edit view into the Create view. However, if I do this, the Create page is not displayed anymore.
I have the following model:
public class ProjectComment
{
public int Id { get; set; }
public int? ProjectId { get; set; }
public Project Project { get; set; }
public int RAGStatusId { get; set; }
public RAGStatus RAGStatus { get; set; }
public string StatusComment { get; set; }
public string EscalationComment { get; set; }
public string GeneralComment { get; set; }
public double? EOQ { get; set; }
public DateTime LastUpdateDate { get; set; }
public ProjectComment ()
{
this.LastUpdateDate = DateTime.UtcNow;
}
The create form Create.cshtml:
#page
#model SimpleProjectReporting.Pages.ClientDetails.CreateModel
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>ProjectComment</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProjectComment.ProjectId" class="control-label"></label>
<select asp-for="ProjectComment.ProjectId" class="form-control" asp-items="ViewBag.ProjectId"><option value="" default="" selected="">-- Select --</option></select>
</div>
<div class="form-group">
<label asp-for="ProjectComment.RAGStatusId" class="control-label"></label>
<select asp-for="ProjectComment.RAGStatusId" class="form-control" asp-items="ViewBag.RAGStatusId"><option value="" default="" selected="">-- Select --</option></select>
</div>
<div class="form-group">
<label asp-for="ProjectComment.StatusComment" class="control-label"></label>
<input asp-for="ProjectComment.StatusComment" class="form-control" />
<span asp-validation-for="ProjectComment.StatusComment" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProjectComment.EOQ" class="control-label"></label>
<input asp-for="ProjectComment.EOQ" class="form-control" />
<span asp-validation-for="ProjectComment.EOQ" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
The original Create.cshtml.cs action:
[BindProperty]
public ProjectComment ProjectComment { get; set; }
public IActionResult OnGet()
{
ViewData["ProjectId"] = new SelectList(_context.Project.Where(a => a.IsArchived == false), "Id", "ProjectName");
ViewData["RAGStatusId"] = new SelectList(_context.RAGStatus.Where(a => a.IsActive == true), "Id", "RAGStatusName");
return Page();
}
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.ProjectComment.Add(ProjectComment);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
The modified Create.cshtml.cs OnGet action:
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
ProjectComment = await _context.ProjectComment
.Include(p => p.Project)
.Include(p => p.RAGStatus).FirstOrDefaultAsync(m => m.Id == id);
if (ProjectComment == null)
{
return NotFound();
}
When modifying the action the way I did it, the page is not displayed anymore (404 error).
I would like to populate the create form with the data from the last entry in the database. If there is no comment, the create page would only populate the name of the project.
You are not sending the "id" parameter to your post action I guess.
So could you please try to adding this line under your form tag:
<form method="post">
<input type="hidden" id="ProjectComment.Id" name="id" value="ProjectComment.Id" />
You are trying to reach the last record of your ProjectComment table.
There are more than one methods to find the last record of your data table. But lets keep it simple.
You have an integer based identity column, which is Auto Increment. So you can simply use below methods to reach out the last created data of your table.
In your OnGetAsync() method:
//maxId will be the maximum value of "Id" columns. Which means that the maximum value is the last recorded value.
int maxId = _context.ProjectComment.Max(i => i.Id);
//And this line will bring you the last recorded "ProjectComment" object.
var projectComment = _context.ProjectComment.Find(maxId);
//You can assign it to your above 'ProjectComment' property if you want to..
ProjectComment = projectComment
Now, since you've find the last recorded data in your database, you can use that object.
Firstly, thanks to Burak for providing the above solution, which works when you want to display the last row in the table. This helped me solving my issue by using the same approach and finding the record based on the Id of the record.
I have amended the code from the Create.cshtml.cs file as follows:
public async Task<IActionResult> OnGetAsync(int? id, int projectid)
{
//This will find the "ProjectComment" object.
var projectComment = _context.ProjectComment.Find(id);
//This will display the 'ProjectComment' on the page
ProjectComment = projectComment;
if (id == null)
{
ProjectComment = projectComment;
ViewData["ProjectId"] = new SelectList(_context.Project, "Id", "ProjectName", projectid);
return Page();
}
ViewData["ProjectId"] = new SelectList(_context.Project, "Id", "ProjectName");
return Page();
}
I am using the int projectid to populate the drop down menu of the project when there is no comment create yet.

Process ModelState errors from WebAPI Blazor (server-side) call

When I am making a WebAPI call I want to find out how to pass back ModelState errors to my Blazor application.
The DataAnnotations all validate correctly but, if I do any other types of validation (once past the ModelState.IsValid call), I can't get those errors that I add to the ModelState to pass back to Blazor. What am I missing?
Blazor page
...
<EditForm Model="#_user" OnValidSubmit="#Update">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="container">
<div class="row">
<div class="col-md-3">Name:</div>
<div class="col-md-9"><InputText id="Name" #bind-Value="#_user.UserName" class="form-control" /></div>
</div>
#if (_user.isNew)
{
<div class="row">
<div class="col-md-3">Password:</div>
<div class="col-md-9"><InputText id="Name" #bind-Value="#_user.Password" class="form-control" type="password" /></div>
</div>
<div class="row">
<div class="col-md-3">Validate your password:</div>
<div class="col-md-9"><InputText id="Name" #bind-Value="#_user.ValidatePassword" class="form-control" type="password" /></div>
</div>
}
<div class="row">
<div class="col-md-3">Email:</div>
<div class="col-md-9"><InputText id="Name" #bind-Value="#_user.Email" class="form-control" /></div>
</div>
<div class="row">
<div class="col-md-3">Roles:</div>
<div class="col-md-9">
#foreach (IdentityRole role in _roles)
{
bool isChecked = _user.UserRoles.Any(r => r.Id == role.Id);
<input type="checkbox" Id="#role.Id" name="#role.Id"
Class="form-control" checked="#isChecked" #onchange="#(e => RoleChecked(e, role.Id))" />
#role.Name
}
</div>
</div>
<button type="submit" class="btn btn-#buttonClass">#buttonText</button>
</div>
</EditForm>
}
#functions {
[Parameter]
string id { get; set; } = "";
private UserViewModel _user { get; set; }
...
private async Task Update()
{
if (id != "")
{
await apiClient.UpdateUserAsync(_user);
}
else
{
await apiClient.AddUserAsync(_user);
}
UriHelper.NavigateTo("admin/users");
}
...
WebAPI Controller
[HttpPost]
public async Task<IActionResult> Post([FromBody] UserViewModel applicationUser)
{
if (applicationUser == null)
{
_logger.LogError($"ApplicationUser object null");
return BadRequest("ApplicationUser object is null");
}
if (!ModelState.IsValid)
{
_logger.LogWarn("ApplicationUser object invalid");
return BadRequest(ModelState);
}
else
{
_logger.LogDebug($"Creating ApplicationUser {applicationUser.UserName}");
var obj = new ApplicationUser();
obj.Map(applicationUser);
IdentityResult result = await _userManager.CreateAsync(obj, applicationUser.Password);
if (result.Succeeded)
{
//put the newly created user back on top of the parameter for role creation
applicationUser.Map(obj);
await IdentityHelpers.UpdateUserRoles(_userManager, applicationUser);
return CreatedAtRoute("", new { id = applicationUser.Id }, applicationUser);
}
else
{
_logger.LogWarn("ApplicationUser object could not be created");
result.Errors.ToList().ForEach(e => ModelState.AddModelError(e.Code, e.Description));
return BadRequest(ModelState);
}
}
}
How do I pass back ModelState errors to Blazor so that it will respond to those in the same way that it would DataAnnotations (or model validation)?

How to send retrieved multiple value array data from view to controller to insert it by loop that add new row until less than count

Here is Model
public class candidate_votes
{
public int ff_id_fk { get; set; }
public int cmember_id { get; set; }
public int cparty_id { get; set; }
public int cand_votos { get; set; }
}
Here is View that i am showing data to insert this data into db so that data is in multi values means bulkdata i want every row add in db untill count
foreach (var doc in Model)
{
<div class="row justify-content-center">
<div class="col-sm-2">
<label>Candidate Name</label>
<p>#doc.member_name</p>
<input type="hidden" name="cmember_id[]" value="#doc.member_id" class="form-control" />
</div>
<div class="col-sm-2">
<label>Party Name</label>
<p>#doc.party_name</p>
<input type="hidden" name="cparty_id[]" value="#doc.party_id_fk" class="form-control" />
</div>
<div class="col-sm-2">
<div class="form-group">
<label>Total Votes</label>
<input type="text" name="cand_votos[]" class="form-control" />
</div>
</div>
</div>
}
Here is controller which i am using to post data and on same time first i get data on view with other controller than i am posting that data to this controller
public ActionResult ps_formForty(candidate_votes cand )
{
Dictionary<string, string> data2 = new Dictionary<string, string>();
for (int i = 0; i < cand.cmember_id.count; i++)
{
data2.Add("cmember_id", (cand.cmember_id).ToString());
data2.Add("cparty_id", cand.cparty_id.ToString());
data2.Add("cand_votos", cand.cand_votos.ToString());
DbObject.Insert("candidate_votes", data2);
}
return View();
}
i want something like this and but i couuld not apply loop on candidate_votes cand object
Thanks in Advance
Just placed index in view for array of object
Then get list of object from your action method.
#Model System.Generic.Collection.List
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#*Note : Replace Your Controller Name*#
#using (Html.BeginForm("ps_formForty", "Home", FormMethod.Post))
{
var i = 0;
foreach (var doc in Model)
{
<div class="row justify-content-center">
<div class="col-sm-2">
<label>Candidate Name</label>
<p>#doc.member_name</p>
<input type="hidden" name="list[#i].cmember_id" value="#doc.cmember_id" class="form-control" />
</div>
<div class="col-sm-2">
<label>Party Name</label>
<p>#doc.party_name</p>
<input type="hidden" name="list[#i].cparty_id" value="#doc.cparty_id" class="form-control" />
</div>
<div class="col-sm-2">
<div class="form-group">
<label>Total Votes</label>
<input type="text" name="cand_votos[#i]" class="form-control" />
</div>
</div>
</div>
i++;
}
<input type="submit">
}
[HttpPost]
public ActionResult ps_formForty(List<candidate_votes> cand)
{
Dictionary<string, string> data2 = new Dictionary<string, string>();
foreach (var item in cand)
{
data2.Add("cmember_id", (item.cmember_id).ToString());
data2.Add("cparty_id", item.cparty_id.ToString());
data2.Add("cand_votos", item.cand_votos.ToString());
DbObject.Insert("candidate_votes", data2);
}
return View();
}
As #Stephen says. You can only loop a collection and not a simple class. You need to return an array from the UI. The C# goes something like this:
public ActionResult ps_formForty(List<candidate_votes> cand )
{
Dictionary<string, string> data2 = new Dictionary<string, string>();
for (int i = 0; i < cand.count; i++)
{
data2.Add("cmember_id", (cand.cmember_id).ToString());
data2.Add("cparty_id", cand.cparty_id.ToString());
data2.Add("cand_votos", cand.cand_votos.ToString());
DbObject.Insert("candidate_votes", data2);
}
return View();
}
From the JavaScript, you may pass the array of items.
$.ajax({
type: "POST",
data: array,
url: url,
success: function(msg){
//Your code
}
});
On the other hand, if you do not have an array, and only one object of candidate_votes, then you should not use a loop. Because there is only one object there.
public ActionResult ps_formForty(candidate_votes cand )
{
Dictionary<string, string> data2 = new Dictionary<string, string>();
data2.Add("cmember_id", (cand.cmember_id).ToString());
data2.Add("cparty_id", cand.cparty_id.ToString());
data2.Add("cand_votos", cand.cand_votos.ToString());
DbObject.Insert("candidate_votes", data2);
return View();
}
If what you want is the number of letters in the cmember_id, then use cmember_id.Length instead of cmember_id.count.

Resources