Search code examples
c#asp.netasp.net-core.net-coreasp.net-identity

ASP.Net Core Identity User Edit Page passing back null roles


I am trying to design and code a view that allows the administrator to edit user details, including role.

However when my viewModel is passed back in the post method the Roles list is null. I can't see why!

Here is my code -

My view model -

using FNSD.Models;
using System.ComponentModel.DataAnnotations;

namespace FNSD.ViewModels
{
    public class UserViewModel
    {
        public string UserId { get; set; } = string.Empty;

        public string Username { get; set; } = string.Empty;

        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;

        public List<RoleViewModel> Roles { get; set; }

        [Required]
        [Display(Name = "User Hospital")]
        public int UserHospitalId { get; set; }

        public List<LkpHospital> Hospitals { get; set; } = new List<LkpHospital> { };

        [Display(Name = "User Phone Number")]
        public string PhoneNumber { get; set; } = string.Empty;
    }

    public class RoleViewModel
    {
        public string RoleName { get; set; }
        public bool IsSelected { get; set; }
    }
}

My view -

div class="row">
    <div class="col-md-4">
        <form id="registerForm" method="post" asp-controller="UserList" asp-action="UserEdit">

            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>

            <input type="hidden" asp-for="UserId" />
            <input type="hidden" asp-for="Username" />

            <div class="form-group mb-3">
                <input asp-for="Email" class="form-control" aria-required="true" placeholder="Please enter new email." />
                <label asp-for="Email" class="form-label"></label>
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            
            <div class="form-group mb-3">
                <input asp-for="PhoneNumber" class="form-control" placeholder="Please enter phone number." />
                <label asp-for="PhoneNumber" class="form-label"></label>
                <span asp-validation-for="PhoneNumber" class="text-danger"></span>
            </div>

            <div class="form-group mb-3">
                <select asp-for="UserHospitalId" aria-required="true" class="form-control" asp-items="@(new SelectList(Model.Hospitals, "HospitalId", "Hospital"))" placeholder="role"></select>
                <label asp-for="UserHospitalId"></label>
                <span asp-validation-for="UserHospitalId" class="text-danger"></span>
            </div>

            <div class="form-group">
                <label>Roles</label>
                @foreach (var role in Model.Roles)
                {
                    <div class="form-radio">
                        <input type="radio" value="@role.IsSelected" asp-for="@role.IsSelected" />
                        <label asp-for="@role.IsSelected">@role.RoleName</label>
                    </div>
                }
            </div>

            <br />

            <div class="form-floating">
                <a asp-controller="UserList" method="get" asp-action="UserList" class="btn btn-danger btn-block">
                    Cancel
                </a>
                <button type="submit" class="btn btn-primary float-end" style='margin-right:16px'>Save Changes</button>
                <a asp-controller="UserList" method="get" style='margin-right:16px' asp-action="ResetPassword" asp-route-userEmail="@Model.Email" class="btn btn-primary btn-block float-end">
                    Reset Password
                </a>
            </div>
        </form>
    </div>
</div>

My Get and Post methods -

[HttpGet]
public async Task<IActionResult> UserEdit(string userEmail)
{
    if (string.IsNullOrEmpty(userEmail))
        return NotFound();

    var user = await _userManager.FindByEmailAsync(userEmail);
    if (user == null)
        return NotFound();

    var userRoles = await _userManager.GetRolesAsync(user);

    var allRoles = _roleManager.Roles.ToList();

    var viewModel = new UserViewModel()
    {
        UserId = user.Id,
        Username = user.UserName,
        Email = user.Email,
        PhoneNumber = user.PhoneNumber,

        Roles = allRoles.Select(r => new RoleViewModel
        {
            RoleName = r.Name,
            IsSelected = userRoles.Contains(r.Name)
        }).ToList(),

        UserHospitalId = _usersHosptialRepository.GetHospitalIdsFromUserId(user.Id).Last().id,
        Hospitals = _lookupRepository.EnabledHospitals().ToList(),
    };

    return View("UserEdit", viewModel);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UserEdit(UserViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByEmailAsync(viewModel.Email);
        if (user == null)
            return NotFound();

        user.UserName = viewModel.Email;
        user.Email = viewModel.Email;

        var userRoles = await _userManager.GetRolesAsync(user);
        var selectedRoles = viewModel.Roles.Where(r => r.IsSelected).Select(r => r.RoleName).ToList();
        var result = await _userManager.UpdateAsync(user);

        if (result.Succeeded)
        {
            // Remove user from roles not selected
            var rolesToRemove = userRoles.Except(selectedRoles).ToList();
            if (rolesToRemove.Any())
            {
                await _userManager.RemoveFromRolesAsync(user, rolesToRemove);
            }

            // Add user to roles selected
            var rolesToAdd = selectedRoles.Except(userRoles).ToList();
            if (rolesToAdd.Any())
            {
                await _userManager.AddToRolesAsync(user, rolesToAdd);
            }

            return RedirectToAction("Index"); // Redirect to list page or another suitable action
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    List<UserListViewModel> users = await _userListControllerHelper.MapToViewModel(_context);
    return View("UserList", users);
}

I don't understand why my Roles list in the view model is null, can someone help?

Thanks


Solution

  • Model Binding Bind the property by name. The correct name should be:Roles[index].IsSelected/Roles[index].RoleName. But if you want achieve selecting one radio button will automatically deselect any previously selected one. These radio button should be in the same group which means they should be with the same name.

    A quick way is to use checkbox instead of radio button:

    <div class="form-group">
        <label>Roles</label>
        @for (int i = 0; i < Model.Roles.Count; i++)
        {
            <div class="form-radio">
                <!-- Bind the IsSelected property using asp-for, but set the value based on the RoleName -->
                <input type="checkbox" asp-for="Roles[i].IsSelected" name="Roles[@i].IsSelected" value="true"  />
                <label>@Model.Roles[i].RoleName</label>
                
                <!-- Hidden input to ensure RoleName is posted back -->
                <input type="hidden" asp-for="Roles[i].RoleName" value="@Model.Roles[i].RoleName" />
            </div>
        }
    </div>
    

    If you must use radio button, you should add a property to your UserViewModel to hold the name of the selected role:

    Model

    public class UserViewModel
    {
        public string UserId { get; set; } = string.Empty;
    
        //.....
        public string? SelectedRole { get; set; }
    
    }
    

    View

    <div class="form-group">
        <label>Roles</label>
        @for (int i = 0; i < Model.Roles.Count; i++)
        {
            <div class="form-radio">
                <!-- Hidden input to capture the role name -->
                <input type="hidden" asp-for="Roles[i].RoleName" value="@Model.Roles[i].RoleName" />
    
                <!-- Radio button for role selection -->
                <input type="radio" name="SelectedRole" value="@Model.Roles[i].RoleName" @(Model.Roles[i].IsSelected ? "checked" : "") />
                <label>@Model.Roles[i].RoleName</label>
            </div>
        }
    </div>